You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/06/12 17:43:22 UTC

(airavata-portals) 01/02: remove now-unused files, and migrate to uv/ty/ruff setup

This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git

commit eff0b04318d86abb39582c652c7a023854b16584
Author: yasithdev <ya...@icloud.com>
AuthorDate: Thu Jun 11 01:12:21 2026 -0400

    remove now-unused files, and migrate to uv/ty/ruff setup
---
 CLAUDE.md                                          |   26 +-
 airavata-django-portal/.dockerignore               |    1 +
 .../.github/workflows/build-and-test.yaml          |   61 +-
 airavata-django-portal/.gitignore                  |    4 +
 airavata-django-portal/.python-version             |    1 +
 airavata-django-portal/Dockerfile                  |   29 +-
 airavata-django-portal/README.md                   |   86 +-
 .../django_airavata/app_config.py                  |    3 +-
 .../django_airavata/apps/admin/admin.py            |    2 -
 .../django_airavata/apps/admin/apps.py             |  105 +-
 .../admin/management/commands/archive_user_data.py |  162 --
 .../management/commands/unarchive_user_data.py     |   42 -
 .../apps/admin/migrations/0001_initial.py          |   35 -
 .../apps/admin/migrations/__init__.py              |    0
 .../django_airavata/apps/admin/models.py           |   17 -
 .../django_airavata/apps/admin/tests.py            |    2 -
 .../django_airavata/apps/admin/urls.py             |   41 +-
 .../django_airavata/apps/admin/views.py            |   46 +-
 .../django_airavata/apps/api/apps.py               |    6 +-
 .../django_airavata/apps/api/downloads.py          |   18 +-
 .../django_airavata/apps/api/exceptions.py         |    5 +-
 .../django_airavata/apps/api/helpers.py            |  107 +-
 .../apps/api/migrations/0001_initial.py            |   23 -
 .../0001_squashed_0004_auto_20190625_1938.py       |   42 -
 .../apps/api/migrations/0002_auto_20190513_2037.py |   27 -
 .../apps/api/migrations/0002_auto_20190711_1438.py |   28 -
 .../apps/api/migrations/0003_auto_20190625_1926.py |   20 -
 .../apps/api/migrations/0003_auto_20190716_1549.py |   30 -
 .../apps/api/migrations/0004_auto_20190625_1938.py |   23 -
 .../apps/api/migrations/0004_auto_20191112_2134.py |   30 -
 .../apps/api/migrations/0005_delete_user_files.py  |   18 -
 ...template_applicationtemplatecontextprocessor.py |   39 -
 .../api/migrations/0007_applicationsettings.py     |   21 -
 .../api/migrations/0007_notificationextension.py   |   24 -
 .../api/migrations/0008_merge_20220601_1951.py     |   14 -
 ...icationsettings_queue_settings_calculator_id.py |   18 -
 .../apps/api/migrations/__init__.py                |    0
 .../django_airavata/apps/api/models.py             |   75 -
 .../django_airavata/apps/api/output_views.py       |  265 +--
 .../django_airavata/apps/api/proto_render.py       |   22 +-
 .../django_airavata/apps/api/serializers.py        |  102 +-
 .../django_airavata/apps/api/signals.py            |    6 +-
 .../apps/api/tests/test_data_products_contract.py  |   69 +-
 .../api/tests/test_full_experiment_contract.py     |  292 ++-
 .../tests/test_group_resource_profiles_contract.py |  142 +-
 .../test_per_protocol_job_submission_contract.py   |  110 +-
 .../apps/api/tests/test_proto_render.py            |   93 +-
 .../django_airavata/apps/api/tests/test_views.py   |  314 +--
 .../django_airavata/apps/api/tus.py                |   18 +-
 .../django_airavata/apps/api/urls.py               |  221 ++-
 .../django_airavata/apps/api/view_utils.py         |  144 +-
 .../django_airavata/apps/api/views.py              | 1542 ++++++++-------
 .../django_airavata/apps/api/web.py                |  353 ++--
 .../django_airavata/apps/auth/apps.py              |    4 +-
 .../django_airavata/apps/auth/decorators.py        |   39 +
 .../django_airavata/apps/auth/iam_admin_client.py  |   95 +-
 .../django_airavata/apps/auth/middleware.py        |   99 +-
 .../apps/auth/migrations/0001_initial.py           |   27 -
 .../apps/auth/migrations/0002_emailtemplate.py     |   25 -
 .../migrations/0003_default_email_templates.py     |   57 -
 .../auth/migrations/0004_password_reset_request.py |   64 -
 .../auth/migrations/0005_auto_20191211_2011.py     |   66 -
 .../auth/migrations/0006_emailverification_next.py |   20 -
 .../auth/migrations/0007_auto_20200917_1610.py     |   43 -
 .../auth/migrations/0008_auto_20210422_1838.py     |   59 -
 .../0009_alter_emailverification_next.py           |   18 -
 .../auth/migrations/0009_auto_20210625_1725.py     |   41 -
 .../0010_userprofile_username_initialized.py       |   18 -
 .../0011_remove_userprofile_username_locked.py     |   17 -
 .../auth/migrations/0012_merge_20211210_2041.py    |   14 -
 .../auth/migrations/0013_auto_20220118_1650.py     |   20 -
 .../auth/migrations/0014_auto_20220217_2255.py     |   24 -
 ...ld_extendeduserprofilefieldlink_extendeduser.py |  107 -
 .../auth/migrations/0015_auto_20220329_1708.py     |   72 -
 .../0016_extendeduserprofilefield_required.py      |   18 -
 .../auth/migrations/0017_auto_20220616_1831.py     |   55 -
 ...4_auto_20220217_2255_0017_auto_20220616_1831.py |   14 -
 .../auth/migrations/0019_auto_20221118_1323.py     |   24 -
 .../auth/migrations/0020_remove_account_models.py  |   47 -
 .../apps/auth/migrations/__init__.py               |    0
 .../django_airavata/apps/auth/models.py            |   34 +-
 .../django_airavata/apps/auth/signals.py           |   28 +-
 .../templates/django_airavata_auth/auth_error.html |   11 -
 .../apps/auth/tests/test_signals.py                |   76 +-
 .../apps/auth/token_authentication.py              |   39 +-
 .../django_airavata/apps/auth/urls.py              |   37 +-
 .../django_airavata/apps/auth/utils.py             |  181 +-
 .../django_airavata/apps/auth/views.py             |  300 +--
 .../django_airavata/apps/dataparsers/admin.py      |    2 -
 .../django_airavata/apps/dataparsers/apps.py       |   18 +-
 .../apps/dataparsers/migrations/__init__.py        |    0
 .../django_airavata/apps/dataparsers/models.py     |    1 -
 .../django_airavata/apps/dataparsers/tests.py      |    2 -
 .../django_airavata/apps/dataparsers/urls.py       |   14 +-
 .../django_airavata/apps/dataparsers/views.py      |   26 +-
 .../django_airavata/apps/groups/admin.py           |    2 -
 .../django_airavata/apps/groups/apps.py            |   16 +-
 .../django_airavata/apps/groups/forms.py           |   38 -
 .../apps/groups/migrations/__init__.py             |    0
 .../django_airavata/apps/groups/models.py          |    2 -
 .../django_airavata/apps/groups/tests.py           |    2 -
 .../django_airavata/apps/groups/urls.py            |    9 +-
 .../django_airavata/apps/groups/views.py           |   35 +-
 .../django_airavata/apps/workspace/admin.py        |    2 -
 .../django_airavata/apps/workspace/apps.py         |   42 +-
 .../apps/workspace/migrations/0001_initial.py      |   24 -
 .../workspace/migrations/0002_delete_user_files.py |   18 -
 .../apps/workspace/migrations/__init__.py          |    0
 .../django_airavata/apps/workspace/models.py       |    2 -
 .../django_airavata/apps/workspace/tests.py        |    2 -
 .../django_airavata/apps/workspace/urls.py         |   37 +-
 .../django_airavata/apps/workspace/views.py        |  201 +-
 .../commons/dynamic_apps/__init__.py               |   10 +-
 .../commons/dynamic_apps/context_processors.py     |    6 +-
 .../django_airavata/commons/dynamic_apps/urls.py   |    3 +-
 .../django_airavata/context_processors.py          |  129 +-
 .../django_airavata/log_utils.py                   |    2 +-
 .../django_airavata/middleware.py                  |    5 +-
 airavata-django-portal/django_airavata/settings.py |  446 +++--
 .../django_airavata/settings_local.py.ide          |  130 --
 .../django_airavata/settings_local.py.sample       |   23 -
 .../django_airavata/uploadhandler.py               |   15 +-
 airavata-django-portal/django_airavata/urls.py     |   39 +-
 airavata-django-portal/django_airavata/views.py    |   61 +-
 airavata-django-portal/django_airavata/wsgi.py     |    4 +-
 .../docs/assets/cms_default_theme.pdf              |  Bin 5036749 -> 0 bytes
 airavata-django-portal/docs/cms.md                 |  277 ---
 airavata-django-portal/docs/dev/wagtail_export.md  |  195 --
 airavata-django-portal/manage.py                   |    4 +-
 airavata-django-portal/mkdocs.yml                  |    4 -
 airavata-django-portal/pyproject.toml              |   87 +
 airavata-django-portal/requirements-dev.txt        |  Bin 1228 -> 0 bytes
 airavata-django-portal/requirements-mysql.txt      |    1 -
 airavata-django-portal/requirements.txt            |   11 -
 airavata-django-portal/runtests.py                 |    2 +-
 airavata-django-portal/scripts/start-server.sh     |    3 +-
 airavata-django-portal/setup.cfg                   |   25 -
 airavata-django-portal/setup.py                    |   60 -
 airavata-django-portal/tests/settings.py           |   22 +-
 airavata-django-portal/tox.ini                     |   14 -
 airavata-django-portal/uv.lock                     | 2038 ++++++++++++++++++++
 pyrightconfig.json                                 |   25 -
 142 files changed, 5676 insertions(+), 5177 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 31867789a..8ddfc606a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,7 +10,7 @@ Monorepo of web portals, SDKs, and tools built on top of Apache Airavata. Contai
 
 | Project | Stack | Purpose |
 |---------|-------|---------|
-| `airavata-django-portal` | Django 3.2 + Vue 2 | Reference science gateway (main portal) |
+| `airavata-django-portal` | Django 5.2 + Vue 2 | Reference science gateway (main portal) |
 | `airavata-research-portal` | React 19 + Vite + TypeScript | CyberShuttle research platform (newest) |
 | `airavata-custos-portal` | Django + Vue 2 | Identity, group, and permissions management UI |
 | `airavata-mft-portal` | Django + Webpack | Managed File Transfer dashboard |
@@ -28,13 +28,10 @@ Monorepo of web portals, SDKs, and tools built on top of Apache Airavata. Contai
 ```bash
 cd airavata-django-portal
 
-# Backend setup
-python3 -m venv venv && source venv/bin/activate
-pip install -r requirements.txt
-pip install -r requirements-dev.txt
-python manage.py migrate
-python manage.py load_cms_data new_default_theme
-python manage.py runserver                        # Port 8000
+# Backend setup (uv-managed; Python 3.12, .venv). The portal has NO database —
+# no migrate step. Persistence goes through the Airavata gRPC API + cache.
+uv sync                                           # install deps (incl. editable airavata-python-sdk)
+uv run python manage.py runserver                 # Port 8000
 
 # Frontend (builds all 8 JS packages)
 ./build_js.sh
@@ -44,12 +41,13 @@ cd django_airavata/apps/workspace
 yarn
 yarn run serve
 
-# Tests
-./runtests.py                                     # Django backend tests
+# Tests / lint / types
+uv run ./runtests.py                              # Django backend tests
 ./test_js.sh                                      # Frontend tests (Jest)
-flake8 .                                          # Python lint
+uv run ruff check .                               # Python lint (ruff)
+uv run ruff format .                              # Python auto-format (ruff)
+uv run ty check                                   # Python type check (ty)
 ./lint_js.sh                                      # JS lint (ESLint + Prettier)
-autopep8 -i -aaa -r . && isort .                  # Auto-format Python
 ```
 
 ### Django Apps
@@ -59,7 +57,7 @@ Each app under `django_airavata/apps/` is self-contained with its own frontend:
 | App | Purpose |
 |-----|---------|
 | `workspace` | Main job/project workspace (Vue 2 + web components) |
-| `api` | REST API endpoints + DRF serializers |
+| `api` | REST API endpoints (plain Django, proto-native renderers) |
 | `auth` | OAuth/Keycloak login + user management |
 | `admin` | Admin dashboard |
 | `groups` | Group management |
@@ -67,7 +65,7 @@ Each app under `django_airavata/apps/` is self-contained with its own frontend:
 
 ### Architecture
 
-- **Backend**: Django + DRF, Thrift-based Airavata client, Wagtail CMS for page content
+- **Backend**: Django (no database, no DRF), gRPC Airavata client via the editable `airavata-python-sdk`, Keycloak-only token auth; page "chrome" comes from settings (no Wagtail)
 - **Frontend**: Vue 2 per app, webpack/Vue CLI builds, each app has colocated `static/` with its own build config
 - **Dynamic apps**: Plugin-style extensions discovered via Python entry points (powered by `django_airavata.commons`)
 - **Web components**: Workspace app builds Vue components as reusable web components
diff --git a/airavata-django-portal/.dockerignore b/airavata-django-portal/.dockerignore
index f1930b1ee..a20476044 100644
--- a/airavata-django-portal/.dockerignore
+++ b/airavata-django-portal/.dockerignore
@@ -1,5 +1,6 @@
 **/node_modules
 venv
+.venv
 **.pyc
 .git
 **/dist
diff --git a/airavata-django-portal/.github/workflows/build-and-test.yaml b/airavata-django-portal/.github/workflows/build-and-test.yaml
index 9e3cc4e31..3243e89cf 100644
--- a/airavata-django-portal/.github/workflows/build-and-test.yaml
+++ b/airavata-django-portal/.github/workflows/build-and-test.yaml
@@ -69,40 +69,35 @@ jobs:
         shell: cmd
 
   build:
-    # ubuntu-22 doesn't support Python 3.6
-    runs-on: ubuntu-20.04
-    needs: build-js
-    strategy:
-      matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: airavata-django-portal
     steps:
-      - uses: actions/checkout@v3
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
         with:
-          python-version: ${{ matrix.python-version }}
-          cache: pip
-          cache-dependency-path: |
-            requirements.txt
-            requirements-dev.txt
-      - name: Install dependencies
-        run: |
-          pip install --upgrade pip setuptools wheel
-          pip install -r requirements-dev.txt
-      - name: Run Django Migrate and Check
-        run: |
-          cp django_airavata/settings_local.py.sample django_airavata/settings_local.py
-          python manage.py migrate
-          python manage.py check
-      - name: Run flake8
-        run: |
-          flake8 .
-      # Need the built frontend client code before running some of the Django tests
-      # that use the Django test Client
-      - uses: actions/download-artifact@v3
+          path: airavata-django-portal
+      # The portal's only Airavata dependency, airavata-python-sdk, is resolved
+      # from a sibling apache/airavata checkout (see [tool.uv.sources] in
+      # pyproject.toml -> ../../airavata/airavata-python-sdk). It is not on PyPI.
+      - uses: actions/checkout@v4
         with:
-          name: built-javascript
-          path: django_airavata
+          repository: apache/airavata
+          path: airavata
+      - name: Install uv (and Python 3.12)
+        uses: astral-sh/setup-uv@v5
+        with:
+          python-version: "3.12"
+          enable-cache: true
+      - name: Install dependencies
+        run: uv sync
+      - name: Lint (ruff)
+        run: uv run ruff check .
+      - name: Format check (ruff)
+        run: uv run ruff format --check .
+      - name: Type check (ty)
+        run: uv run ty check
+      - name: Django system check (no database, no migrations)
+        run: uv run python manage.py check
       - name: Run Django unit tests
-        run: |
-          ./runtests.py
+        run: uv run ./runtests.py
diff --git a/airavata-django-portal/.gitignore b/airavata-django-portal/.gitignore
index 9aa90e212..4824918ba 100644
--- a/airavata-django-portal/.gitignore
+++ b/airavata-django-portal/.gitignore
@@ -17,3 +17,7 @@ package-lock.json
 site/
 .tox/
 yarn-error.log
+.venv/
+.pytest_cache/
+.ruff_cache/
+__pycache__/
diff --git a/airavata-django-portal/.python-version b/airavata-django-portal/.python-version
new file mode 100644
index 000000000..e4fba2183
--- /dev/null
+++ b/airavata-django-portal/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/airavata-django-portal/Dockerfile b/airavata-django-portal/Dockerfile
index e385ed674..e7ef0ba4a 100644
--- a/airavata-django-portal/Dockerfile
+++ b/airavata-django-portal/Dockerfile
@@ -62,20 +62,31 @@ RUN yarn run build
 
 
 
-FROM python:3.10-slim as server-stage
+FROM python:3.12-slim as server-stage
 
-ENV PYTHONUNBUFFERED 1
+ENV PYTHONUNBUFFERED=1 \
+    UV_COMPILE_BYTECODE=1 \
+    UV_LINK_MODE=copy \
+    PATH="/code/.venv/bin:$PATH"
+
+# uv for dependency management (https://docs.astral.sh/uv/).
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
 
 EXPOSE 8000
 
 WORKDIR /code
-COPY requirements.txt requirements-mysql.txt ./
-COPY setup.* ./
-COPY README.md .
-RUN apt-get update && apt-get install -y git gcc g++ zlib1g-dev libjpeg-dev default-libmysqlclient-dev
-RUN pip install --upgrade pip setuptools wheel --no-cache
-RUN pip install -r requirements.txt --no-cache
-RUN pip install -r requirements-mysql.txt --no-cache
+RUN apt-get update && apt-get install -y --no-install-recommends \
+        git gcc g++ zlib1g-dev libjpeg-dev && \
+    rm -rf /var/lib/apt/lists/*
+
+# NOTE: the portal's only Airavata dependency is the airavata-python-sdk, resolved
+# from a sibling apache/airavata checkout via [tool.uv.sources] in pyproject.toml.
+# It is not yet on PyPI, so the build context must make it available at
+# ../../airavata/airavata-python-sdk (e.g. build from the workspace root with an
+# appropriate context) or the pin must point at a published release.
+COPY pyproject.toml uv.lock README.md ./
+COPY ./django_airavata ./django_airavata
+RUN uv sync --frozen --no-dev
 
 # Copy in a default settings_local.py file
 COPY ./django_airavata/settings_local.py.sample ./django_airavata/settings_local.py
diff --git a/airavata-django-portal/README.md b/airavata-django-portal/README.md
index 491543ad5..62c154c46 100644
--- a/airavata-django-portal/README.md
+++ b/airavata-django-portal/README.md
@@ -27,50 +27,31 @@ any yarn commands. See
 [the Yarn package manager](https://classic.yarnpkg.com/lang/en/) for information
 on how to install Yarn 1 (Classic).
 
-1.  Checkout this project and create a virtual environment.
+This project uses [uv](https://docs.astral.sh/uv/) for Python dependency
+management. The portal has **no database** — there is nothing to migrate, and
+all persistence goes through the Airavata gRPC API and the cache.
+
+1.  Check out the project and install dependencies.
 
     ```
     git clone https://github.com/apache/airavata-django-portal.git
     cd airavata-django-portal
-    python3 -m venv venv
-    source venv/bin/activate
-    pip install --upgrade pip setuptools wheel
-    pip install -r requirements.txt
+    uv sync
     ```
 
-    - **Windows note**: Use ```venv\Scripts\activate``` instead of ```source venv/bin/activate```
-      <!-- https://docs.python.org/3/library/venv.html -->
-
-    - **macOS note**: to install the MySQL dependencies you need to have the
-      MySQL development headers and libraries installed. Also, on macOS you need
-      to have openssl installed. See the
-      [mysqlclient-python installation notes](https://github.com/PyMySQL/mysqlclient-python#install)
-      for more details.
-
-2.  Create a local settings file.
-
-    - Option 1 (**recommended**). The best way to get a local settings file is
-      to download one from an existing Airavata Django Portal instance. If you
-      have Admin access, you can log in, go to _Settings_ and then _Developer
-      Console_ (/admin/developers) and download a `settings_local.py` file for
-      local development. Save it to the `django_airavata/` directory.
-
-    - Option 2. Otherwise, if you know the hostname and ports of an Airavata
-      deployment, you can copy `django_airavata/settings_local.py.sample` to
-      `django_airavata/settings_local.py` and edit the contents to match your
-      Keycloak and Airavata server deployments.
+    `uv sync` creates `.venv/` (Python 3.12) and installs everything, including
+    the editable `airavata-python-sdk` from a sibling `apache/airavata` checkout
+    (see `[tool.uv.sources]` in `pyproject.toml`). Prefix commands with
+    `uv run` (e.g. `uv run python manage.py ...`) or activate `.venv` directly.
 
-      ```
-      cp django_airavata/settings_local.py.sample django_airavata/settings_local.py
-      ```
-
-3.  Run Django migrations
+2.  Create a local settings file by copying the sample and editing it to match
+    your Keycloak and Airavata server deployment.
 
     ```
-    python manage.py migrate
+    cp django_airavata/settings_local.py.sample django_airavata/settings_local.py
     ```
 
-4.  Build the JavaScript sources. There are a few JavaScript packages in the
+3.  Build the JavaScript sources. There are a few JavaScript packages in the
     source tree, colocated with the Django apps in which they are used. The
     `build_js.sh` script will build them all.
 
@@ -78,21 +59,15 @@ on how to install Yarn 1 (Classic).
     ./build_js.sh
     ```
 
-    - **Window note**: on Windows, run `.\build_js.bat` instead
+    - **Windows note**: on Windows, run `.\build_js.bat` instead
 
-5.  Load the default Wagtail CMS pages.
+4.  Run the server.
 
     ```
-    python manage.py load_cms_data new_default_theme
+    uv run python manage.py runserver
     ```
 
-6.  Run the server
-
-    ```
-    python manage.py runserver
-    ```
-
-7.  Point your browser to http://localhost:8000.
+5.  Point your browser to http://localhost:8000.
 
 ## Docker instructions
 
@@ -114,14 +89,7 @@ following:
      -p 8000:8000 airavata-django-portal
    ```
 
-3. Load an initial set of Wagtail pages (theme). You only need to do this when
-   starting the container for the first time.
-
-   ```
-   docker exec CONTAINER_ID python manage.py load_cms_data new_default_theme
-   ```
-
-4. Point your browser to http://localhost:8000.
+3. Point your browser to http://localhost:8000.
 
 ### Multi-architecture images
 
@@ -178,12 +146,16 @@ Apache Airavata website.
 
 ### Setting up development environment
 
-Run `pip install -r requirements-dev.txt` to install development and testing
-libraries.
+`uv sync` installs the development and testing tools (ruff, ty) alongside the
+runtime dependencies. Lint, format, and type-check the Python code with:
+
+```
+uv run ruff check .       # lint
+uv run ruff format .      # auto-format
+uv run ty check           # type check
+```
 
-Use a code editor that integrates with editorconfig and flake8. I also recommend
-autopep8 for automatically formatting code to follow the PEP8 guidelines.
-Prettier is used for formatting JavaScript and Vue.js code.
+Prettier and ESLint are used for JavaScript and Vue.js code (see `lint_js.sh`).
 
 See the docs for more information on
 [developing the backend](./docs/dev/developing_backend.md) and
@@ -191,7 +163,7 @@ See the docs for more information on
 
 ### Running Django Tests
 
-Run `./runtests.py` to run the Django unit tests.
+Run `uv run ./runtests.py` to run the Django unit tests.
 
 ## License
 
diff --git a/airavata-django-portal/django_airavata/app_config.py b/airavata-django-portal/django_airavata/app_config.py
index 718e851f4..2a977f85f 100644
--- a/airavata-django-portal/django_airavata/app_config.py
+++ b/airavata-django-portal/django_airavata/app_config.py
@@ -1,4 +1,3 @@
-
 import logging
 from abc import ABC, abstractmethod
 from importlib import import_module
@@ -44,7 +43,7 @@ class AiravataAppConfig(AppConfig, ABC):
 def get_url_app_name(app_config):
     """Return the urls namespace for the given AppConfig instance."""
     urls = get_app_urls(app_config)
-    return getattr(urls, 'app_name', None)
+    return getattr(urls, "app_name", None)
 
 
 def get_app_urls(app_config):
diff --git a/airavata-django-portal/django_airavata/apps/admin/admin.py b/airavata-django-portal/django_airavata/apps/admin/admin.py
deleted file mode 100644
index b97a94f64..000000000
--- a/airavata-django-portal/django_airavata/apps/admin/admin.py
+++ /dev/null
@@ -1,2 +0,0 @@
-
-# Register your models here.
diff --git a/airavata-django-portal/django_airavata/apps/admin/apps.py b/airavata-django-portal/django_airavata/apps/admin/apps.py
index a9a664203..a713fa443 100644
--- a/airavata-django-portal/django_airavata/apps/admin/apps.py
+++ b/airavata-django-portal/django_airavata/apps/admin/apps.py
@@ -1,76 +1,81 @@
-
 from django_airavata.app_config import AiravataAppConfig
 
 
 class AdminConfig(AiravataAppConfig):
-    name = 'django_airavata.apps.admin'
-    label = 'django_airavata_admin'
-    verbose_name = 'Settings'
+    name = "django_airavata.apps.admin"
+    label = "django_airavata_admin"
+    verbose_name = "Settings"
     app_order = 100
-    url_home = 'django_airavata_admin:home'
-    fa_icon_class = 'fa-cog'
+    url_home = "django_airavata_admin:home"
+    fa_icon_class = "fa-cog"
     app_description = """
         Configure and share resources with other users.
     """
     nav = [
         {
-            'label': 'Application Catalog',
-            'icon': 'fa fa-cogs',
-            'url': 'django_airavata_admin:app_catalog',
-            'active_prefixes': ['applications'],
-            'enabled': lambda req: (req.is_gateway_admin or
-                                    req.is_read_only_gateway_admin),
+            "label": "Application Catalog",
+            "icon": "fa fa-cogs",
+            "url": "django_airavata_admin:app_catalog",
+            "active_prefixes": ["applications"],
+            "enabled": lambda req: (
+                req.is_gateway_admin or req.is_read_only_gateway_admin
+            ),
         },
         {
-            'label': 'Manage Users',
-            'icon': 'fa fa-users',
-            'url': 'django_airavata_admin:users',
-            'active_prefixes': ['users', 'extended-user-profile'],
-            'enabled': lambda req: (req.is_gateway_admin or
-                                    req.is_read_only_gateway_admin),
+            "label": "Manage Users",
+            "icon": "fa fa-users",
+            "url": "django_airavata_admin:users",
+            "active_prefixes": ["users", "extended-user-profile"],
+            "enabled": lambda req: (
+                req.is_gateway_admin or req.is_read_only_gateway_admin
+            ),
         },
         {
-            'label': 'Experiment Statistics',
-            'icon': 'fa fa-chart-bar',
-            'url': 'django_airavata_admin:experiment-statistics',
-            'active_prefixes': ['experiment-statistics'],
-            'enabled': lambda req: (req.is_gateway_admin or
-                                    req.is_read_only_gateway_admin),
+            "label": "Experiment Statistics",
+            "icon": "fa fa-chart-bar",
+            "url": "django_airavata_admin:experiment-statistics",
+            "active_prefixes": ["experiment-statistics"],
+            "enabled": lambda req: (
+                req.is_gateway_admin or req.is_read_only_gateway_admin
+            ),
         },
         {
-            'label': 'Credential Store',
-            'icon': 'fa fa-lock',
-            'url': 'django_airavata_admin:credential_store',
-            'active_prefixes': ['credentials']
+            "label": "Credential Store",
+            "icon": "fa fa-lock",
+            "url": "django_airavata_admin:credential_store",
+            "active_prefixes": ["credentials"],
         },
         {
-            'label': 'Group Resource Profile',
-            'icon': 'fa fa-server',
-            'url': 'django_airavata_admin:group_resource_profile',
-            'active_prefixes': ['group-resource-profiles']
+            "label": "Group Resource Profile",
+            "icon": "fa fa-server",
+            "url": "django_airavata_admin:group_resource_profile",
+            "active_prefixes": ["group-resource-profiles"],
         },
         {
-            'label': 'Gateway Resource Profile',
-            'icon': 'fa fa-tasks',
-            'url': 'django_airavata_admin:gateway_resource_profile',
-            'active_prefixes': ['gateway-resource-profile'],
-            'enabled': lambda req: (req.is_gateway_admin or
-                                    req.is_read_only_gateway_admin)
+            "label": "Gateway Resource Profile",
+            "icon": "fa fa-tasks",
+            "url": "django_airavata_admin:gateway_resource_profile",
+            "active_prefixes": ["gateway-resource-profile"],
+            "enabled": lambda req: (
+                req.is_gateway_admin or req.is_read_only_gateway_admin
+            ),
         },
         {
-            'label': 'Manage Notices',
-            'icon': 'fa fa-bell',
-            'url': 'django_airavata_admin:notices',
-            'active_prefixes': ['notices'],
-            'enabled': lambda req: (req.is_gateway_admin or
-                                    req.is_read_only_gateway_admin)
+            "label": "Manage Notices",
+            "icon": "fa fa-bell",
+            "url": "django_airavata_admin:notices",
+            "active_prefixes": ["notices"],
+            "enabled": lambda req: (
+                req.is_gateway_admin or req.is_read_only_gateway_admin
+            ),
         },
         {
-            'label': 'Developer Console',
-            'icon': 'fa fa-code',
-            'url': 'django_airavata_admin:developers',
-            'active_prefixes': ['developers'],
-            'enabled': lambda req: (req.is_gateway_admin or
-                                    req.is_read_only_gateway_admin)
+            "label": "Developer Console",
+            "icon": "fa fa-code",
+            "url": "django_airavata_admin:developers",
+            "active_prefixes": ["developers"],
+            "enabled": lambda req: (
+                req.is_gateway_admin or req.is_read_only_gateway_admin
+            ),
         },
     ]
diff --git a/airavata-django-portal/django_airavata/apps/admin/management/commands/archive_user_data.py b/airavata-django-portal/django_airavata/apps/admin/management/commands/archive_user_data.py
deleted file mode 100644
index bab8be533..000000000
--- a/airavata-django-portal/django_airavata/apps/admin/management/commands/archive_user_data.py
+++ /dev/null
@@ -1,162 +0,0 @@
-import datetime
-import logging
-import os
-import shutil
-import tarfile
-import tempfile
-from pathlib import Path
-from typing import Iterator
-
-from django.conf import settings
-from django.core.management.base import BaseCommand, CommandError
-from django.db import transaction
-from django.utils import timezone
-
-from django_airavata.apps.admin import models
-
-logger = logging.getLogger(__name__)
-
-
-class Command(BaseCommand):
-    help = "Create an archive of user data directories and optionally clean them up"
-
-    def add_arguments(self, parser):
-        parser.add_argument('--dry-run',
-                            action='store_true',
-                            help="Print the list of files/directories that would be archived then exit",
-                            default=False)
-        parser.add_argument('--max-age',
-                            help="Max age of files/directories in days. Any that are older will be archived.",
-                            type=int,
-                            default=getattr(settings, "GATEWAY_USER_DATA_ARCHIVE_MAX_AGE_DAYS", None))
-
-    def handle(self, *args, **options):
-        try:
-            # Take --max-age from the command line first, then from the setting
-            max_age_setting = options['max_age']
-            if max_age_setting is None:
-                raise CommandError("Setting GATEWAY_USER_DATA_ARCHIVE_MAX_AGE_DAYS is not configured and --max-age option missing.")
-
-            max_age = timezone.now() - datetime.timedelta(days=max_age_setting)
-            entries_to_archive = self.get_archive_entries(older_than=max_age)
-            gateway_id = settings.GATEWAY_ID
-
-            archive_directory = Path(settings.GATEWAY_USER_DATA_ARCHIVE_DIRECTORY)
-            archive_directory.mkdir(parents=True, exist_ok=True)
-
-            with tempfile.TemporaryDirectory(dir=archive_directory) as tmpdir:
-                archive_basename = f"archive_{gateway_id}_older_than_{max_age.strftime('%Y-%m-%d-%H-%M-%S')}"
-                archive_list_filename = f"{archive_basename}.txt"
-                archive_list_filepath = os.path.join(tmpdir, archive_list_filename)
-                entry_count = 0
-                with open(archive_list_filepath, "wt") as archive_list_file:
-                    for entry in entries_to_archive:
-                        entry_count = entry_count + 1
-                        archive_list_file.write(f"{entry.path}\n")
-
-                # If nothing matching to archive, just exit
-                if entry_count == 0:
-                    self.stdout.write(self.style.WARNING("Nothing to archive, exiting now"))
-                    return
-
-                # if dry run, just print file and exit
-                if options['dry_run']:
-                    self.stdout.write(f"DRY RUN: printing {archive_list_filename}, then exiting")
-                    with open(os.path.join(tmpdir, archive_list_filename)) as archive_list_file:
-                        for line in archive_list_file:
-                            self.stdout.write(line)
-                    self.stdout.write(self.style.SUCCESS("DRY RUN: exiting now"))
-                    return
-
-                # otherwise, generate a tarball in tmpdir
-                archive_tarball_filename = f"{archive_basename}.tgz"
-                archive_tarball_filepath = os.path.join(tmpdir, archive_tarball_filename)
-                with tarfile.open(archive_tarball_filepath, "w:gz") as tarball:
-                    with open(os.path.join(tmpdir, archive_list_filename)) as archive_list_file:
-                        for line in archive_list_file:
-                            tarball.add(line.strip())
-
-                minimum_bytes_size = settings.GATEWAY_USER_DATA_ARCHIVE_MINIMUM_ARCHIVE_SIZE_GB * 1024 ** 3
-                if os.stat(archive_tarball_filepath).st_size < minimum_bytes_size:
-                    self.stdout.write(self.style.WARNING("Aborting, archive size is not large enough to proceed (size less than GATEWAY_USER_DATA_ARCHIVE_MINIMUM_ARCHIVE_SIZE_GB)"))
-                    # Exit early
-                    return
-
-                # Move the archive files into the final destination
-                shutil.move(archive_list_filepath, archive_directory / archive_list_filename)
-                shutil.move(archive_tarball_filepath, archive_directory / archive_tarball_filename)
-
-                self.stdout.write(self.style.SUCCESS(f"Created tarball: {archive_directory / archive_tarball_filename}"))
-
-            # Now we'll remove any files/directories that were in the archive
-            # and create database records for the archive
-            try:
-                # If any error occurs in this block, the transaction will be rolled back
-                with transaction.atomic():
-                    user_data_archive = models.UserDataArchive(
-                        archive_name=archive_tarball_filename,
-                        archive_path=os.fspath(archive_directory / archive_tarball_filename),
-                        max_modification_time=max_age)
-                    user_data_archive.save()
-                    # delete archived entries
-                    with open(archive_directory / archive_list_filename) as archive_list_file:
-                        for archive_path in archive_list_file:
-                            archive_path = archive_path.strip()
-                            if os.path.isfile(archive_path):
-                                os.remove(archive_path)
-                            elif os.path.isdir(archive_path):
-                                shutil.rmtree(archive_path)
-                            else:
-                                self.stdout.write(self.style.WARNING(f"Cannot delete {archive_path} as it is neither a file nor directory, perhaps was already removed"))
-                            archive_entry = models.UserDataArchiveEntry(user_data_archive=user_data_archive, entry_path=archive_path)
-                            archive_entry.save()
-            except Exception as e:
-                self.stdout.write(self.style.ERROR("Failed while deleting archived data, attempting to roll back"))
-                with tarfile.open(archive_directory / archive_tarball_filename) as tf:
-                    tf.extractall(path="/")
-                logger.exception(f"[archive_user_data] Failed to delete archived files, but unarchived from tarball {archive_directory / archive_tarball_filename}", exc_info=e)
-                raise CommandError(f"Failed to delete archived files, but unarchived from tarball {archive_directory / archive_tarball_filename}") from e
-
-            self.stdout.write(self.style.SUCCESS("Successfully removed archived user data"))
-        except CommandError:
-            raise
-        except Exception:
-            logger.exception("[archive_user_data] Failed to create user data archive")
-
-    def get_archive_entries(self, older_than: datetime.datetime) -> Iterator[os.DirEntry]:
-
-        GATEWAY_USER_DIR = settings.USER_STORAGES['default']['OPTIONS']['directory']
-
-        with os.scandir(GATEWAY_USER_DIR) as user_dirs:
-            for user_dir_entry in user_dirs:
-                # Skip over any files (shouldn't be any but who knows)
-                if not user_dir_entry.is_dir():
-                    continue
-                # Skip over shared directories
-                if self._is_shared_directory(user_dir_entry):
-                    continue
-                with os.scandir(user_dir_entry.path) as project_dirs:
-                    for project_dir_entry in project_dirs:
-                        yield from self._scan_project_dir_for_archive_entries(
-                            project_dir_entry=project_dir_entry,
-                            older_than=older_than)
-
-    def _scan_project_dir_for_archive_entries(self, project_dir_entry: os.DirEntry, older_than: datetime.datetime) -> Iterator[os.DirEntry]:
-        # archive files here but not directories
-        if project_dir_entry.is_file() and project_dir_entry.stat().st_mtime < older_than.timestamp():
-            yield project_dir_entry
-        # Skip over shared directories
-        if project_dir_entry.is_dir() and not self._is_shared_directory(project_dir_entry):
-            with os.scandir(project_dir_entry.path) as experiment_dirs:
-                for experiment_dir_entry in experiment_dirs:
-                    if experiment_dir_entry.stat().st_mtime < older_than.timestamp():
-                        yield experiment_dir_entry
-
-    def _is_shared_directory(self, dir_entry: os.DirEntry) -> bool:
-        if not dir_entry.is_dir():
-            return False
-        shared_dirs = getattr(settings, "GATEWAY_DATA_SHARED_DIRECTORIES", {})
-        for shared_dir in shared_dirs.values():
-            if os.path.samefile(dir_entry.path, shared_dir["path"]):
-                return True
-        return False
diff --git a/airavata-django-portal/django_airavata/apps/admin/management/commands/unarchive_user_data.py b/airavata-django-portal/django_airavata/apps/admin/management/commands/unarchive_user_data.py
deleted file mode 100644
index dddb35b0d..000000000
--- a/airavata-django-portal/django_airavata/apps/admin/management/commands/unarchive_user_data.py
+++ /dev/null
@@ -1,42 +0,0 @@
-
-import os
-import pathlib
-import tarfile
-
-from django.core.management.base import BaseCommand
-
-from django_airavata.apps.admin import models
-
-
-class Command(BaseCommand):
-    help = "Unarchive a user data archive"
-
-    def add_arguments(self, parser):
-        parser.add_argument('archive_file',
-                            help="Archive file (ending in .tgz) that was created by the archive_user_data")
-        parser.add_argument('-m', '--reset-modification',
-                            help="Set modification timestamp to current time for all entries in archive",
-                            action="store_true")
-
-    def handle(self, *args, **options):
-        with tarfile.open(options["archive_file"]) as tf:
-            tf.extractall(path="/")
-
-        # mark archive as rolled back
-        archive_name = os.path.basename(options["archive_file"])
-        try:
-            archive = models.UserDataArchive.objects.get(archive_name=archive_name)
-            archive.rolled_back = True
-            archive.save()
-        except models.UserDataArchive.DoesNotExist:
-            self.stdout.write(self.style.ERROR(f"Could not find UserDataArchive database record for {archive_name}"))
-
-        if options["reset_modification"]:
-            root = pathlib.Path('/')
-            with tarfile.open(options["archive_file"]) as tf:
-                for tarinfo in tf:
-                    path = root / tarinfo.path
-                    path.touch()
-            self.stdout.write("Updated modification timestamps for all entries")
-
-        self.stdout.write(self.style.SUCCESS(f"Successfully unarchived {options['archive_file']}"))
diff --git a/airavata-django-portal/django_airavata/apps/admin/migrations/0001_initial.py b/airavata-django-portal/django_airavata/apps/admin/migrations/0001_initial.py
deleted file mode 100644
index 6223ce73c..000000000
--- a/airavata-django-portal/django_airavata/apps/admin/migrations/0001_initial.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Generated by Django 3.2.18 on 2023-04-17 14:10
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='UserDataArchive',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-                ('updated_date', models.DateTimeField(auto_now=True)),
-                ('archive_name', models.CharField(max_length=255)),
-                ('archive_path', models.TextField()),
-                ('rolled_back', models.BooleanField(default=False)),
-                ('max_modification_time', models.DateTimeField()),
-            ],
-        ),
-        migrations.CreateModel(
-            name='UserDataArchiveEntry',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('entry_path', models.TextField()),
-                ('user_data_archive', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_airavata_admin.userdataarchive')),
-            ],
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/admin/migrations/__init__.py b/airavata-django-portal/django_airavata/apps/admin/migrations/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/airavata-django-portal/django_airavata/apps/admin/models.py b/airavata-django-portal/django_airavata/apps/admin/models.py
deleted file mode 100644
index 2471f9afc..000000000
--- a/airavata-django-portal/django_airavata/apps/admin/models.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from django.db import models
-
-# Create your models here.
-
-
-class UserDataArchive(models.Model):
-    created_date = models.DateTimeField(auto_now_add=True)
-    updated_date = models.DateTimeField(auto_now=True)
-    archive_name = models.CharField(max_length=255)
-    archive_path = models.TextField()
-    rolled_back = models.BooleanField(default=False)
-    max_modification_time = models.DateTimeField()
-
-
-class UserDataArchiveEntry(models.Model):
-    user_data_archive = models.ForeignKey(UserDataArchive, on_delete=models.CASCADE)
-    entry_path = models.TextField()
diff --git a/airavata-django-portal/django_airavata/apps/admin/tests.py b/airavata-django-portal/django_airavata/apps/admin/tests.py
deleted file mode 100644
index 492902048..000000000
--- a/airavata-django-portal/django_airavata/apps/admin/tests.py
+++ /dev/null
@@ -1,2 +0,0 @@
-
-# Create your tests here.
diff --git a/airavata-django-portal/django_airavata/apps/admin/urls.py b/airavata-django-portal/django_airavata/apps/admin/urls.py
index f52672714..1b60d6e35 100644
--- a/airavata-django-portal/django_airavata/apps/admin/urls.py
+++ b/airavata-django-portal/django_airavata/apps/admin/urls.py
@@ -2,19 +2,32 @@ from django.urls import path, re_path
 
 from . import views
 
-app_name = 'django_airavata_admin'
+app_name = "django_airavata_admin"
 urlpatterns = [
-    re_path(r'^$', views.home, name='home'),
-    re_path(r'^applications/', views.app_catalog, name='app_catalog'),
-    re_path(r'^credentials/', views.credential_store, name='credential_store'),
-    re_path(r'^experiment-statistics/', views.experiment_statistics,
-            name="experiment-statistics"),
-    re_path(r'^group-resource-profiles/', views.group_resource_profile,
-            name='group_resource_profile'),
-    re_path(r'^gateway-resource-profile/', views.gateway_resource_profile,
-            name='gateway_resource_profile'),
-    re_path(r'^notices/', views.notices, name='notices'),
-    re_path(r'^users/', views.users, name='users'),
-    path('extended-user-profile/', views.extended_user_profile, name="extended_user_profile"),
-    path('developers/', views.developers, name='developers'),
+    re_path(r"^$", views.home, name="home"),
+    re_path(r"^applications/", views.app_catalog, name="app_catalog"),
+    re_path(r"^credentials/", views.credential_store, name="credential_store"),
+    re_path(
+        r"^experiment-statistics/",
+        views.experiment_statistics,
+        name="experiment-statistics",
+    ),
+    re_path(
+        r"^group-resource-profiles/",
+        views.group_resource_profile,
+        name="group_resource_profile",
+    ),
+    re_path(
+        r"^gateway-resource-profile/",
+        views.gateway_resource_profile,
+        name="gateway_resource_profile",
+    ),
+    re_path(r"^notices/", views.notices, name="notices"),
+    re_path(r"^users/", views.users, name="users"),
+    path(
+        "extended-user-profile/",
+        views.extended_user_profile,
+        name="extended_user_profile",
+    ),
+    path("developers/", views.developers, name="developers"),
 ]
diff --git a/airavata-django-portal/django_airavata/apps/admin/views.py b/airavata-django-portal/django_airavata/apps/admin/views.py
index e8be1042c..cf2967782 100644
--- a/airavata-django-portal/django_airavata/apps/admin/views.py
+++ b/airavata-django-portal/django_airavata/apps/admin/views.py
@@ -1,71 +1,71 @@
-from django.contrib.auth.decorators import login_required
 from django.shortcuts import redirect, render
 from django.urls import reverse
 
+from django_airavata.apps.auth.decorators import login_required
+
 
 @login_required
 def home(request):
     if request.is_gateway_admin or request.is_read_only_gateway_admin:
-        return redirect(reverse('django_airavata_admin:app_catalog'))
+        return redirect(reverse("django_airavata_admin:app_catalog"))
     else:
-        return redirect(
-            reverse('django_airavata_admin:group_resource_profile'))
+        return redirect(reverse("django_airavata_admin:group_resource_profile"))
 
 
 @login_required
 def app_catalog(request):
-    request.active_nav_item = 'app_catalog'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "app_catalog"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def credential_store(request):
-    request.active_nav_item = 'credential_store'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "credential_store"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def compute_resource(request):
-    return render(request, 'admin/compute_resource.html')
+    return render(request, "admin/compute_resource.html")
 
 
 @login_required
 def group_resource_profile(request):
-    request.active_nav_item = 'group_resource_profile'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "group_resource_profile"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def gateway_resource_profile(request):
-    request.active_nav_item = 'gateway_resource_profile'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "gateway_resource_profile"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def notices(request):
-    request.active_nav_item = 'notices'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "notices"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def users(request):
-    request.active_nav_item = 'users'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "users"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def extended_user_profile(request):
-    request.active_nav_item = 'users'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "users"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def experiment_statistics(request):
-    request.active_nav_item = 'experiment-statistics'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "experiment-statistics"
+    return render(request, "admin/admin_base.html")
 
 
 @login_required
 def developers(request):
-    request.active_nav_item = 'developers'
-    return render(request, 'admin/admin_base.html')
+    request.active_nav_item = "developers"
+    return render(request, "admin/admin_base.html")
diff --git a/airavata-django-portal/django_airavata/apps/api/apps.py b/airavata-django-portal/django_airavata/apps/api/apps.py
index e42f0b217..bafb141cb 100644
--- a/airavata-django-portal/django_airavata/apps/api/apps.py
+++ b/airavata-django-portal/django_airavata/apps/api/apps.py
@@ -4,13 +4,13 @@ from django.apps import AppConfig
 
 
 class ApiConfig(AppConfig):
-    name = 'django_airavata.apps.api'
-    label = 'django_airavata_api'
+    name = "django_airavata.apps.api"
+    label = "django_airavata_api"
 
     def ready(self):
         from . import signals  # noqa
         from . import output_views
 
         # Load and create instances of each output view provider
-        for entry_point in entry_points(group='airavata.output_view_providers'):
+        for entry_point in entry_points(group="airavata.output_view_providers"):
             output_views.OUTPUT_VIEW_PROVIDERS[entry_point.name] = entry_point.load()()
diff --git a/airavata-django-portal/django_airavata/apps/api/downloads.py b/airavata-django-portal/django_airavata/apps/api/downloads.py
index dbef27283..cde47c97a 100644
--- a/airavata-django-portal/django_airavata/apps/api/downloads.py
+++ b/airavata-django-portal/django_airavata/apps/api/downloads.py
@@ -6,6 +6,7 @@ Container) off the retired ``airavata_django_portal_sdk`` and onto the gRPC
 storage facade (``request.airavata.storage``). Individual-file downloads go
 through the ``download``/``download-file`` views instead.
 """
+
 import io
 import logging
 import os
@@ -24,9 +25,9 @@ log = logging.getLogger(__name__)
 @api_view()
 def download_dir(request):
     """Stream a user-storage directory (``?path=``) as a zip archive."""
-    path = request.GET.get('path', "")
+    path = request.GET.get("path", "")
     base = get_valid_filename(os.path.basename(path))
-    filename = (base + ".zip") if base else 'home.zip'
+    filename = (base + ".zip") if base else "home.zip"
     resolved = _user_storage_path(path, request=request)
     entries = _zip_entries(request.airavata.storage, resolved)
     return _zip_response(request.airavata.storage, filename, entries)
@@ -35,11 +36,11 @@ def download_dir(request):
 @api_view()
 def download_experiment_dir(request, experiment_id=None):
     """Stream an experiment's output directory (``?path=``) as a zip archive."""
-    path = request.GET.get('path', "")
+    path = request.GET.get("path", "")
     experiment = request.airavata.research.get_experiment(experiment_id)
     exp_name = get_valid_filename(experiment.experiment_name)
     base = get_valid_filename(os.path.basename(path))
-    filename = f'{exp_name}_{base}.zip' if base else f'{exp_name}.zip'
+    filename = f"{exp_name}_{base}.zip" if base else f"{exp_name}.zip"
     resolved = _user_storage_path(path, experiment_id=experiment_id, request=request)
     entries = _zip_entries(request.airavata.storage, resolved)
     return _zip_response(request.airavata.storage, filename, entries)
@@ -70,13 +71,12 @@ def _zip_response(storage, filename, entries):
     # for large directories in production this should be replaced with a true
     # streaming zip (the previous zipstream-new behavior).
     buf = io.BytesIO()
-    with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as zf:
-        for archive_name, abs_path, size in entries:
+    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED, allowZip64=True) as zf:
+        for archive_name, abs_path, _size in entries:
             zf.writestr(archive_name, storage.download_file(abs_path).content)
     buf.seek(0)
-    response = StreamingHttpResponse(
-        _iter_buffer(buf), content_type='application/zip')
-    response['Content-Disposition'] = f'attachment; filename={filename}'
+    response = StreamingHttpResponse(_iter_buffer(buf), content_type="application/zip")
+    response["Content-Disposition"] = f"attachment; filename={filename}"
     return response
 
 
diff --git a/airavata-django-portal/django_airavata/apps/api/exceptions.py b/airavata-django-portal/django_airavata/apps/api/exceptions.py
index 7a673b54b..b8193c795 100644
--- a/airavata-django-portal/django_airavata/apps/api/exceptions.py
+++ b/airavata-django-portal/django_airavata/apps/api/exceptions.py
@@ -7,6 +7,5 @@ from django.http import JsonResponse
 # web.py dispatch exception mapping (e.g. the tus upload-finish handler). The
 # grpc->HTTP mapping that ``custom_exception_handler`` used to provide now lives
 # in ``web.exception_to_response`` (``web.GRPC_STATUS_TO_HTTP``).
-def generic_json_exception_response(
-        exc, status=HTTPStatus.INTERNAL_SERVER_ERROR):
-    return JsonResponse({'detail': str(exc)}, status=status)
+def generic_json_exception_response(exc, status=HTTPStatus.INTERNAL_SERVER_ERROR):
+    return JsonResponse({"detail": str(exc)}, status=status)
diff --git a/airavata-django-portal/django_airavata/apps/api/helpers.py b/airavata-django-portal/django_airavata/apps/api/helpers.py
index 1c932afc1..9324444f6 100644
--- a/airavata-django-portal/django_airavata/apps/api/helpers.py
+++ b/airavata-django-portal/django_airavata/apps/api/helpers.py
@@ -1,12 +1,31 @@
 import logging
 
-from django.core.cache import cache
-
 from airavata_sdk.helpers import compute_resources
+from django.core.cache import cache
 
 logger = logging.getLogger(__name__)
 
 
+# Per-gateway notification "show in dashboard" flags live in the cache, not the
+# DB (was the api_notificationextension table): {notification_id: bool}. Portal-
+# only UI state with no proto/SDK equivalent; cache eviction just resets the flag
+# to its False default until an admin sets it again.
+def _notif_dashboard_key(gateway_id):
+    return f"notif_show_in_dashboard:{gateway_id}"
+
+
+def show_in_dashboard_map(gateway_id):
+    """Return ``{notification_id: show_in_dashboard}`` for the gateway."""
+    return cache.get(_notif_dashboard_key(gateway_id), {})
+
+
+def set_show_in_dashboard(gateway_id, notification_id, value):
+    """Set (or clear) the show_in_dashboard flag for one notification."""
+    flags = show_in_dashboard_map(gateway_id)
+    flags[notification_id] = bool(value)
+    cache.set(_notif_dashboard_key(gateway_id), flags)
+
+
 # Per-user workspace preferences (most-recent project/group/compute + per-app
 # favorites) live in the cache, not the DB (was api_workspacepreferences /
 # api_applicationpreferences). Pure UX state; cache eviction just reseeds the
@@ -55,10 +74,11 @@ class _ApplicationPreferencesManager:
     def get(self, application_id):
         if application_id not in self._prefs._favorites:
             from django.core.exceptions import ObjectDoesNotExist
+
             raise ObjectDoesNotExist()
         return _ApplicationPreference(
-            self._prefs, application_id,
-            self._prefs._favorites[application_id])
+            self._prefs, application_id, self._prefs._favorites[application_id]
+        )
 
     def create(self, username=None, application_id=None, favorite=False):
         self._prefs._set_favorite(application_id, favorite)
@@ -77,19 +97,19 @@ class WorkspacePreferences:
         self.username = username
         self.most_recent_project_id = data.get("most_recent_project_id")
         self.most_recent_group_resource_profile_id = data.get(
-            "most_recent_group_resource_profile_id")
+            "most_recent_group_resource_profile_id"
+        )
         self.most_recent_compute_resource_id = data.get(
-            "most_recent_compute_resource_id")
+            "most_recent_compute_resource_id"
+        )
         self._favorites = dict(data.get("application_preferences", {}))
         self.applicationpreferences_set = _ApplicationPreferencesManager(self)
 
     def _as_dict(self):
         return {
             "most_recent_project_id": self.most_recent_project_id,
-            "most_recent_group_resource_profile_id":
-                self.most_recent_group_resource_profile_id,
-            "most_recent_compute_resource_id":
-                self.most_recent_compute_resource_id,
+            "most_recent_group_resource_profile_id": self.most_recent_group_resource_profile_id,
+            "most_recent_compute_resource_id": self.most_recent_compute_resource_id,
             "application_preferences": self._favorites,
         }
 
@@ -121,40 +141,55 @@ class WorkspacePreferencesHelper:
         return workspace_preferences
 
     def _create_default(self, request, username):
-        defaults = compute_resources.resolve_workspace_defaults(
-            request.airavata)
-        return WorkspacePreferences(username, {
-            "most_recent_project_id": defaults["most_recent_project_id"],
-            "most_recent_group_resource_profile_id":
-                defaults["most_recent_group_resource_profile_id"],
-        })
+        defaults = compute_resources.resolve_workspace_defaults(request.airavata)
+        return WorkspacePreferences(
+            username,
+            {
+                "most_recent_project_id": defaults["most_recent_project_id"],
+                "most_recent_group_resource_profile_id": defaults[
+                    "most_recent_group_resource_profile_id"
+                ],
+            },
+        )
 
     def _check(self, request, prefs):
         "Validate preference values and update as needed."
-        if (not prefs.most_recent_project_id or
-                not self._can_write(request, prefs.most_recent_project_id)):
-            most_recent_project_id = (
-                compute_resources.most_recent_writeable_project_id(
-                    request.airavata))
+        if not prefs.most_recent_project_id or not self._can_write(
+            request, prefs.most_recent_project_id
+        ):
+            most_recent_project_id = compute_resources.most_recent_writeable_project_id(
+                request.airavata
+            )
             if most_recent_project_id is not None:
-                logger.info("_check: updating most_recent_project_id to {}".format(most_recent_project_id))
+                logger.info(
+                    f"_check: updating most_recent_project_id to {most_recent_project_id}"
+                )
                 prefs.most_recent_project_id = most_recent_project_id
                 prefs.save()
             else:
-                logger.warning("_check: no writeable projects found, unsetting most_recent_project_id")
+                logger.warning(
+                    "_check: no writeable projects found, unsetting most_recent_project_id"
+                )
                 prefs.most_recent_project_id = None
                 prefs.save()
         group_resource_profile_ids = (
-            compute_resources.accessible_group_resource_profile_ids(
-                request.airavata))
-        if (not prefs.most_recent_group_resource_profile_id or
-                prefs.most_recent_group_resource_profile_id not in group_resource_profile_ids):
-            first_grp_id = (group_resource_profile_ids[0]
-                            if len(group_resource_profile_ids) > 0
-                            else None)
-            logger.warning(f"_check: updating "
-                           f"most_recent_group_resource_profile_id to "
-                           f"{first_grp_id}")
+            compute_resources.accessible_group_resource_profile_ids(request.airavata)
+        )
+        if (
+            not prefs.most_recent_group_resource_profile_id
+            or prefs.most_recent_group_resource_profile_id
+            not in group_resource_profile_ids
+        ):
+            first_grp_id = (
+                group_resource_profile_ids[0]
+                if len(group_resource_profile_ids) > 0
+                else None
+            )
+            logger.warning(
+                f"_check: updating "
+                f"most_recent_group_resource_profile_id to "
+                f"{first_grp_id}"
+            )
             prefs.most_recent_group_resource_profile_id = first_grp_id
             prefs.save()
 
@@ -163,5 +198,5 @@ class WorkspacePreferencesHelper:
 
     def _can_read(self, request, entity_id):
         return request.airavata.sharing.user_has_access(
-            resource_id=entity_id, user_id=request.user.username,
-            permission_type="READ")
+            resource_id=entity_id, user_id=request.user.username, permission_type="READ"
+        )
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0001_initial.py b/airavata-django-portal/django_airavata/apps/api/migrations/0001_initial.py
deleted file mode 100644
index c59bae5af..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0001_initial.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.20 on 2019-03-05 19:35
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='WorkspacePreferences',
-            fields=[
-                ('username', models.CharField(max_length=64, primary_key=True, serialize=False)),
-                ('most_recent_project_id', models.CharField(max_length=64)),
-            ],
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0001_squashed_0004_auto_20190625_1938.py b/airavata-django-portal/django_airavata/apps/api/migrations/0001_squashed_0004_auto_20190625_1938.py
deleted file mode 100644
index 7251fce2a..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0001_squashed_0004_auto_20190625_1938.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2019-06-25 19:39
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    replaces = [('django_airavata_api', '0001_initial'), ('django_airavata_api', '0002_auto_20190513_2037'), ('django_airavata_api', '0003_auto_20190625_1926'), ('django_airavata_api', '0004_auto_20190625_1938')]
-
-    initial = True
-
-    dependencies = [
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='WorkspacePreferences',
-            fields=[
-                ('username', models.CharField(max_length=64, primary_key=True, serialize=False)),
-                ('most_recent_project_id', models.CharField(max_length=64)),
-            ],
-        ),
-        migrations.CreateModel(
-            name='User_Files',
-            fields=[
-                ('username', models.CharField(max_length=64)),
-                ('file_path', models.TextField()),
-                ('file_dpu', models.CharField(max_length=255, primary_key=True, serialize=False)),
-            ],
-        ),
-        migrations.AlterField(
-            model_name='user_files',
-            name='file_dpu',
-            field=models.CharField(max_length=255, primary_key=True, serialize=False),
-        ),
-        migrations.AddIndex(
-            model_name='user_files',
-            index=models.Index(fields=['username'], name='username_idx'),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0002_auto_20190513_2037.py b/airavata-django-portal/django_airavata/apps/api/migrations/0002_auto_20190513_2037.py
deleted file mode 100644
index 4bd49880a..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0002_auto_20190513_2037.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.20 on 2019-05-13 20:37
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='User_Files',
-            fields=[
-                ('username', models.CharField(max_length=64)),
-                ('file_path', models.TextField()),
-                ('file_dpu', models.CharField(max_length=500, primary_key=True, serialize=False)),
-            ],
-        ),
-        migrations.AddIndex(
-            model_name='user_files',
-            index=models.Index(fields=['username', 'file_path'], name='username_file_path_idx'),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0002_auto_20190711_1438.py b/airavata-django-portal/django_airavata/apps/api/migrations/0002_auto_20190711_1438.py
deleted file mode 100644
index 8b938a780..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0002_auto_20190711_1438.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2019-07-11 14:38
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0001_squashed_0004_auto_20190625_1938'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='User_Notifications',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('username', models.CharField(max_length=64)),
-                ('notification_id', models.CharField(max_length=255)),
-                ('is_read', models.BooleanField(default=False)),
-            ],
-        ),
-        migrations.AlterUniqueTogether(
-            name='user_notifications',
-            unique_together=set([('username', 'notification_id')]),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0003_auto_20190625_1926.py b/airavata-django-portal/django_airavata/apps/api/migrations/0003_auto_20190625_1926.py
deleted file mode 100644
index 3b639ce4a..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0003_auto_20190625_1926.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2019-06-25 19:26
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0002_auto_20190513_2037'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='user_files',
-            name='file_dpu',
-            field=models.CharField(max_length=255, primary_key=True, serialize=False),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0003_auto_20190716_1549.py b/airavata-django-portal/django_airavata/apps/api/migrations/0003_auto_20190716_1549.py
deleted file mode 100644
index 3840aab39..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0003_auto_20190716_1549.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2019-07-16 15:49
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0002_auto_20190711_1438'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='ApplicationPreferences',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('username', models.CharField(max_length=64)),
-                ('application_id', models.CharField(max_length=64)),
-                ('favorite', models.BooleanField(default=False)),
-                ('workspace_preferences', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_airavata_api.WorkspacePreferences')),
-            ],
-        ),
-        migrations.AlterUniqueTogether(
-            name='applicationpreferences',
-            unique_together=set([('username', 'application_id')]),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0004_auto_20190625_1938.py b/airavata-django-portal/django_airavata/apps/api/migrations/0004_auto_20190625_1938.py
deleted file mode 100644
index 245efca84..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0004_auto_20190625_1938.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2019-06-25 19:38
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0003_auto_20190625_1926'),
-    ]
-
-    operations = [
-        migrations.RemoveIndex(
-            model_name='user_files',
-            name='username_file_path_idx',
-        ),
-        migrations.AddIndex(
-            model_name='user_files',
-            index=models.Index(fields=['username'], name='username_idx'),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0004_auto_20191112_2134.py b/airavata-django-portal/django_airavata/apps/api/migrations/0004_auto_20191112_2134.py
deleted file mode 100644
index 994dad65a..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0004_auto_20191112_2134.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2019-11-12 21:34
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0003_auto_20190716_1549'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='workspacepreferences',
-            name='most_recent_compute_resource_id',
-            field=models.CharField(max_length=255, null=True),
-        ),
-        migrations.AddField(
-            model_name='workspacepreferences',
-            name='most_recent_group_resource_profile_id',
-            field=models.CharField(max_length=255, null=True),
-        ),
-        migrations.AlterField(
-            model_name='workspacepreferences',
-            name='most_recent_project_id',
-            field=models.CharField(max_length=255, null=True),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0005_delete_user_files.py b/airavata-django-portal/django_airavata/apps/api/migrations/0005_delete_user_files.py
deleted file mode 100644
index ed22e1276..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0005_delete_user_files.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2020-07-01 15:59
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0004_auto_20191112_2134'),
-    ]
-
-    operations = [
-        migrations.DeleteModel(
-            name='User_Files',
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0006_applicationtemplate_applicationtemplatecontextprocessor.py b/airavata-django-portal/django_airavata/apps/api/migrations/0006_applicationtemplate_applicationtemplatecontextprocessor.py
deleted file mode 100644
index 688cc6377..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0006_applicationtemplate_applicationtemplatecontextprocessor.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Generated by Django 2.2.17 on 2021-05-20 17:13
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('django_airavata_api', '0005_delete_user_files'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='ApplicationTemplate',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('application_module_id', models.CharField(max_length=255, unique=True)),
-                ('template_path', models.TextField()),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('updated', models.DateTimeField(auto_now=True)),
-                ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
-                ('updated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-        migrations.CreateModel(
-            name='ApplicationTemplateContextProcessor',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('callable_path', models.CharField(max_length=255)),
-                ('application_template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='context_processors', to='django_airavata_api.ApplicationTemplate')),
-            ],
-            options={
-                'unique_together': {('application_template', 'callable_path')},
-            },
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0007_applicationsettings.py b/airavata-django-portal/django_airavata/apps/api/migrations/0007_applicationsettings.py
deleted file mode 100644
index 1266cbc48..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0007_applicationsettings.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.2.11 on 2022-05-06 18:47
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0006_applicationtemplate_applicationtemplatecontextprocessor'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='ApplicationSettings',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('application_module_id', models.CharField(max_length=255, unique=True)),
-                ('show_queue_settings', models.BooleanField(default=True)),
-            ],
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0007_notificationextension.py b/airavata-django-portal/django_airavata/apps/api/migrations/0007_notificationextension.py
deleted file mode 100644
index 24a97f22b..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0007_notificationextension.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 3.2.10 on 2022-05-25 21:08
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0006_applicationtemplate_applicationtemplatecontextprocessor'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='NotificationExtension',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('notification_id', models.CharField(max_length=255)),
-                ('showInDashboard', models.BooleanField(default=False)),
-            ],
-            options={
-                'unique_together': {('notification_id',)},
-            },
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0008_merge_20220601_1951.py b/airavata-django-portal/django_airavata/apps/api/migrations/0008_merge_20220601_1951.py
deleted file mode 100644
index 04f0d4f25..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0008_merge_20220601_1951.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Generated by Django 3.2.10 on 2022-06-01 19:51
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0007_applicationsettings'),
-        ('django_airavata_api', '0007_notificationextension'),
-    ]
-
-    operations = [
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py b/airavata-django-portal/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py
deleted file mode 100644
index bc7bf8fbf..000000000
--- a/airavata-django-portal/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.2.15 on 2022-08-25 13:34
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_api', '0008_merge_20220601_1951'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='applicationsettings',
-            name='queue_settings_calculator_id',
-            field=models.CharField(max_length=255, null=True),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/api/migrations/__init__.py b/airavata-django-portal/django_airavata/apps/api/migrations/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/airavata-django-portal/django_airavata/apps/api/models.py b/airavata-django-portal/django_airavata/apps/api/models.py
deleted file mode 100644
index 45c881642..000000000
--- a/airavata-django-portal/django_airavata/apps/api/models.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from django.conf import settings
-from django.db import models
-
-
-# Create your models here.
-class WorkspacePreferences(models.Model):
-    username = models.CharField(max_length=64, primary_key=True)
-    most_recent_project_id = models.CharField(max_length=255, null=True)
-    most_recent_group_resource_profile_id = models.CharField(max_length=255,
-                                                             null=True)
-    most_recent_compute_resource_id = models.CharField(max_length=255,
-                                                       null=True)
-
-    @classmethod
-    def create(cls, username):
-        return WorkspacePreferences(username=username)
-
-
-class ApplicationPreferences(models.Model):
-    username = models.CharField(max_length=64)
-    application_id = models.CharField(max_length=64)
-    favorite = models.BooleanField(default=False)
-    workspace_preferences = models.ForeignKey(WorkspacePreferences,
-                                              on_delete=models.CASCADE)
-
-    class Meta:
-        unique_together = (('username', 'application_id'),)
-
-
-class NotificationExtension(models.Model):
-    class Meta:
-        unique_together = (('notification_id', ),)
-
-    notification_id = models.CharField(max_length=255)
-    showInDashboard = models.BooleanField(default=False)
-
-
-class User_Notifications(models.Model):
-    class Meta:
-        unique_together = (('username', 'notification_id'),)
-
-    username = models.CharField(max_length=64)
-    notification_id = models.CharField(max_length=255)
-    is_read = models.BooleanField(default=False)
-
-
-class ApplicationTemplate(models.Model):
-    application_module_id = models.CharField(max_length=255, unique=True)
-    template_path = models.TextField()
-    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="+")
-    updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="+")
-    created = models.DateTimeField(auto_now_add=True)
-    updated = models.DateTimeField(auto_now=True)
-
-    def __str__(self):
-        return f"{self.application_module_id}: {self.template_path}"
-
-
-class ApplicationTemplateContextProcessor(models.Model):
-    application_template = models.ForeignKey(ApplicationTemplate, on_delete=models.CASCADE, related_name="context_processors")
-    # Use django.util.module_loading.import_string to import
-    # https://docs.djangoproject.com/en/3.2/ref/utils/#module-django.utils.module_loading
-    callable_path = models.CharField(max_length=255)
-
-    class Meta:
-        unique_together = (('application_template', 'callable_path'),)
-
-    def __str__(self):
-        return self.callable_path
-
-
-class ApplicationSettings(models.Model):
-    application_module_id = models.CharField(max_length=255, unique=True)
-    show_queue_settings = models.BooleanField(default=True)
-    queue_settings_calculator_id = models.CharField(max_length=255, null=True)
diff --git a/airavata-django-portal/django_airavata/apps/api/output_views.py b/airavata-django-portal/django_airavata/apps/api/output_views.py
index 7833dc6bf..f26768800 100644
--- a/airavata-django-portal/django_airavata/apps/api/output_views.py
+++ b/airavata-django-portal/django_airavata/apps/api/output_views.py
@@ -1,4 +1,4 @@
-import collections
+import collections.abc
 import inspect
 import json
 import logging
@@ -7,7 +7,7 @@ from functools import partial
 
 import nbformat
 import papermill as pm
-from airavata_sdk.generated.org.apache.airavata.model.application.io.application_io_pb2 import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.application.io.application_io_pb2 import (
     DataType,
 )
 from airavata_sdk.helpers import research_resources, storage_resources
@@ -23,58 +23,46 @@ OUTPUT_VIEW_PROVIDERS = {}
 
 
 class DefaultViewProvider:
-    display_type = 'default'
+    display_type = "default"
     immediate = False
     name = "Default"
 
     def generate_data(
-            self,
-            request,
-            experiment_output,
-            experiment,
-            output_file=None,
-            **kwargs):
-        return {
-        }
+        self, request, experiment_output, experiment, output_file=None, **kwargs
+    ):
+        return {}
 
 
 class ParameterizedNotebookViewProvider:
-    display_type = 'notebook'
+    display_type = "notebook"
     name = "Example Parameterized Notebook View"
     # test_output_file = os.path.join(BASE_DIR, "data", "Gaussian.log")
 
-    def generate_data(self,
-                      request,
-                      experiment_output,
-                      experiment,
-                      output_file=None,
-                      output_dir=None):
+    def generate_data(
+        self, request, experiment_output, experiment, output_file=None, output_dir=None
+    ):
         # use papermill to generate the output notebook
-        output_file_path = os.path.realpath(output_file.name)
+        output_file_path = os.path.realpath(output_file.name)  # ty: ignore[unresolved-attribute]  # provider only invoked with a real output file handle
         pm.execute_notebook(
             os.path.join(BASE_DIR, "path", "to", "notebook.ipynb"),
             # TODO: use TemporaryFile instead
-            '/tmp/output.ipynb',
-            parameters=dict(
-                experiment_output={},
-                experiment={},
-                output_file=output_file_path,
-                output_dir=output_dir
-            )
+            "/tmp/output.ipynb",
+            parameters={
+                "experiment_output": {},
+                "experiment": {},
+                "output_file": output_file_path,
+                "output_dir": output_dir,
+            },
         )
         # TODO: convert the output notebook into html format
-        output_notebook = nbformat.read('/tmp/output.ipynb', as_version=4)
+        output_notebook = nbformat.read("/tmp/output.ipynb", as_version=4)
         html_exporter = HTMLExporter()
-        (body, resources) = html_exporter.from_notebook_node(output_notebook)
+        (body, _resources) = html_exporter.from_notebook_node(output_notebook)
         # TODO: return the HTML output as the output key
-        return {
-            'output': body
-        }
+        return {"output": body}
 
 
-DEFAULT_VIEW_PROVIDERS = {
-    'default': DefaultViewProvider()
-}
+DEFAULT_VIEW_PROVIDERS = {"default": DefaultViewProvider()}
 
 
 def get_output_views(request, experiment, application_interface=None):
@@ -82,32 +70,34 @@ def get_output_views(request, experiment, application_interface=None):
     for output in experiment.experiment_outputs:
         output_views[output.name] = []
         output_view_provider_ids = _get_output_view_providers(
-            output, application_interface)
+            output, application_interface
+        )
         for output_view_provider_id in output_view_provider_ids:
             output_view_provider = None
             if output_view_provider_id in DEFAULT_VIEW_PROVIDERS:
-                output_view_provider = DEFAULT_VIEW_PROVIDERS[
-                    output_view_provider_id]
+                output_view_provider = DEFAULT_VIEW_PROVIDERS[output_view_provider_id]
             elif output_view_provider_id in OUTPUT_VIEW_PROVIDERS:
-                output_view_provider = OUTPUT_VIEW_PROVIDERS[
-                    output_view_provider_id]
+                output_view_provider = OUTPUT_VIEW_PROVIDERS[output_view_provider_id]
             else:
-                logger.warning("Unable to find output view provider with "
-                               "name '{}'".format(output_view_provider_id))
+                logger.warning(
+                    f"Unable to find output view provider with name '{output_view_provider_id}'"
+                )
             if output_view_provider is not None:
                 view_config = {
-                    'provider-id': output_view_provider_id,
-                    'display-type': output_view_provider.display_type,
-                    'name': getattr(output_view_provider, 'name',
-                                    output_view_provider_id),
+                    "provider-id": output_view_provider_id,
+                    "display-type": output_view_provider.display_type,
+                    "name": getattr(
+                        output_view_provider, "name", output_view_provider_id
+                    ),
                 }
-                if getattr(output_view_provider, 'immediate', False):
+                if getattr(output_view_provider, "immediate", False):
                     # Immediately call generate_data function
                     data = _generate_data(
-                        request, output_view_provider, output, experiment)
-                    view_config['data'] = data
+                        request, output_view_provider, output, experiment
+                    )
+                    view_config["data"] = data
                 else:
-                    view_config['data'] = {}
+                    view_config["data"] = {}
                 output_views[output.name].append(view_config)
     return output_views
 
@@ -122,139 +112,149 @@ def _get_output_view_provider(output_view_provider_id):
 
 def _get_output_view_providers(experiment_output, application_interface):
     output_view_providers = []
-    logger.debug("experiment_output={}".format(experiment_output))
+    logger.debug(f"experiment_output={experiment_output}")
     if experiment_output.meta_data:
         try:
             output_metadata = json.loads(experiment_output.meta_data)
-            logger.debug("output_metadata={}".format(output_metadata))
-            if 'output-view-providers' in output_metadata:
-                output_view_providers.extend(
-                    output_metadata['output-view-providers'])
+            logger.debug(f"output_metadata={output_metadata}")
+            if "output-view-providers" in output_metadata:
+                output_view_providers.extend(output_metadata["output-view-providers"])
         except Exception:
             logger.exception(
-                "Failed to parse metadata for output {}".format(
-                    experiment_output.name))
+                f"Failed to parse metadata for output {experiment_output.name}"
+            )
     # Add in any output view providers defined on the application interface
     if application_interface is not None:
         app_output_view_providers = _get_application_output_view_providers(
-            application_interface, experiment_output.name)
+            application_interface, experiment_output.name
+        )
         for view_provider in app_output_view_providers:
             if view_provider not in output_view_providers:
                 output_view_providers.append(view_provider)
-    if 'default' not in output_view_providers:
-        output_view_providers.insert(0, 'default')
+    if "default" not in output_view_providers:
+        output_view_providers.insert(0, "default")
     return output_view_providers
 
 
 def _get_application_output_view_providers(application_interface, output_name):
-    app_output = [o
-                  for o in application_interface.application_outputs
-                  if o.name == output_name]
+    app_output = [
+        o for o in application_interface.application_outputs if o.name == output_name
+    ]
     if len(app_output) == 1:
-        logger.debug("{}: {}".format(output_name, app_output))
+        logger.debug(f"{output_name}: {app_output}")
         app_output = app_output[0]
     else:
         return []
     if app_output.meta_data:
         try:
             output_metadata = json.loads(app_output.meta_data)
-            if 'output-view-providers' in output_metadata:
-                return output_metadata['output-view-providers']
+            if "output-view-providers" in output_metadata:
+                return output_metadata["output-view-providers"]
         except Exception:
-            logger.exception(
-                "Failed to parse metadata for output {}".format(
-                    app_output.name))
+            logger.exception(f"Failed to parse metadata for output {app_output.name}")
     return []
 
 
-def generate_data(request,
-                  output_view_provider_id,
-                  experiment_output_name,
-                  experiment_id,
-                  test_mode=False,
-                  **kwargs):
+def generate_data(
+    request,
+    output_view_provider_id,
+    experiment_output_name,
+    experiment_id,
+    test_mode=False,
+    **kwargs,
+):
     output_view_provider = _get_output_view_provider(output_view_provider_id)
     # TODO if output_view_provider is None, return 404
     experiment = research_resources.get_experiment_proto(
-        request.airavata, experiment_id)
-    experiment_output = [o
-                         for o in experiment.experiment_outputs
-                         if o.name == experiment_output_name]
+        request.airavata, experiment_id
+    )
+    experiment_output = [
+        o for o in experiment.experiment_outputs if o.name == experiment_output_name
+    ]
     # TODO: handle experiment_output not found by name
     experiment_output = experiment_output[0]
     # TODO: add experiment_output_dir
     # convert the extra/interactive arguments to appropriate types
     kwargs = _convert_params_to_type(output_view_provider, kwargs)
-    return _generate_data(request,
-                          output_view_provider,
-                          experiment_output,
-                          experiment,
-                          test_mode=test_mode,
-                          **kwargs)
-
-
-def _generate_data(request,
-                   output_view_provider,
-                   experiment_output,
-                   experiment,
-                   test_mode=False,
-                   **kwargs):
+    return _generate_data(
+        request,
+        output_view_provider,
+        experiment_output,
+        experiment,
+        test_mode=test_mode,
+        **kwargs,
+    )
+
+
+def _generate_data(
+    request,
+    output_view_provider,
+    experiment_output,
+    experiment,
+    test_mode=False,
+    **kwargs,
+):
     output_files = []
     # test_mode can only be used in DEBUG=True mode
     if test_mode and settings.DEBUG:
-        test_output_file = getattr(output_view_provider,
-                                   'test_output_file',
-                                   None)
+        test_output_file = getattr(output_view_provider, "test_output_file", None)
         if test_output_file is None:
             raise Exception(f"test_output_file is not set on {output_view_provider}")
         logger.info(f"Using {test_output_file} instead of regular output file")
-        output_file = open(test_output_file, 'rb')
+        # Handle kept open intentionally: stored in output_files and read by the
+        # view provider downstream; a context manager would close it too early.
+        output_file = open(test_output_file, "rb")  # noqa: SIM115
         output_files.append(output_file)
 
-    elif (experiment_output.value and
-          experiment_output.type in (DataType.URI,
-                                     DataType.URI_COLLECTION,
-                                     DataType.STDOUT,
-                                     DataType.STDERR) and
-          experiment_output.value.startswith("airavata-dp")):
+    elif (
+        experiment_output.value
+        and experiment_output.type
+        in (DataType.URI, DataType.URI_COLLECTION, DataType.STDOUT, DataType.STDERR)
+        and experiment_output.value.startswith("airavata-dp")
+    ):
         data_product_uris = experiment_output.value.split(",")
         output_files.extend(
             storage_resources.download_data_product_files(
-                request.airavata, data_product_uris))
+                request.airavata, data_product_uris
+            )
+        )
 
     generate_data_func = output_view_provider.generate_data
     method_sig = inspect.signature(generate_data_func)
-    if 'output_files' in method_sig.parameters:
+    if "output_files" in method_sig.parameters:
         generate_data_func = partial(generate_data_func, output_files=output_files)
     # TODO: convert experiment and experiment_output to dict/JSON
-    data = generate_data_func(request,
-                              experiment_output,
-                              experiment,
-                              output_file=output_files[0] if len(output_files) > 0 else None,
-                              **kwargs)
+    data = generate_data_func(
+        request,
+        experiment_output,
+        experiment,
+        output_file=output_files[0] if len(output_files) > 0 else None,
+        **kwargs,
+    )
     _process_interactive_params(data)
     return data
 
 
 def _process_interactive_params(data):
-    if 'interactive' in data:
+    if "interactive" in data:
         _convert_options(data)
-        for param in data['interactive']:
-            if 'type' not in param:
-                param['type'] = _infer_interactive_param_type(param)
+        for param in data["interactive"]:
+            if "type" not in param:
+                param["type"] = _infer_interactive_param_type(param)
             # integer type implicitly has a step size of 1
-            if param['type'] == "integer" and 'step' not in param:
-                param['step'] = 1
+            if param["type"] == "integer" and "step" not in param:
+                param["step"] = 1
 
 
 def _convert_options(data):
     """Convert interactive options to explicit text/value dicts."""
-    for param in data['interactive']:
-        if 'options' in param and isinstance(param['options'][0], str):
-            param['options'] = _convert_options_strings(param['options'])
-        elif 'options' in param and isinstance(
-                param['options'][0], collections.Sequence):
-            param['options'] = _convert_options_sequences(param['options'])
+    for param in data["interactive"]:
+        if "options" in param and isinstance(param["options"][0], str):
+            param["options"] = _convert_options_strings(param["options"])
+        elif "options" in param and isinstance(
+            param["options"][0], collections.abc.Sequence
+        ):
+            param["options"] = _convert_options_sequences(param["options"])
 
 
 def _convert_options_strings(options):
@@ -266,7 +266,7 @@ def _convert_options_sequences(options):
 
 
 def _infer_interactive_param_type(param):
-    v = param['value']
+    v = param["value"]
     # Boolean test must come first since bools are also integers
     if isinstance(v, bool):
         return "boolean"
@@ -285,24 +285,27 @@ def _convert_params_to_type(output_view_provider, params):
     # parameters (will only be present if there are interactive parameters)
     meta = json.loads(params.pop("_meta", "{}"))
     for k, v in params.items():
-        meta_type = meta[k]['type'] if k in meta else None
+        meta_type = meta[k]["type"] if k in meta else None
         default_value = None
-        if (k in method_params and
-            method_params[k].default is not inspect.Parameter.empty and
-                method_params[k].default is not None):
+        if (
+            k in method_params
+            and method_params[k].default is not inspect.Parameter.empty
+            and method_params[k].default is not None
+        ):
             default_value = method_params[k].default
         # TODO: handle lists?
         # Handle boolean and numeric values, converting from string
-        if meta_type == 'boolean' or isinstance(default_value, bool):
+        if meta_type == "boolean" or isinstance(default_value, bool):
             params[k] = v == "true"
-        elif meta_type == 'float' or isinstance(default_value, float):
+        elif meta_type == "float" or isinstance(default_value, float):
             params[k] = float(v)
-        elif meta_type == 'integer' or isinstance(default_value, int):
+        elif meta_type == "integer" or isinstance(default_value, int):
             params[k] = int(v)
-        elif meta_type == 'string' or isinstance(default_value, str):
+        elif meta_type == "string" or isinstance(default_value, str):
             params[k] = v
         else:
             logger.warning(
                 f"Unrecognized type for parameter {k}: "
-                f"meta_type={meta_type}, default_value={default_value}")
+                f"meta_type={meta_type}, default_value={default_value}"
+            )
     return params
diff --git a/airavata-django-portal/django_airavata/apps/api/proto_render.py b/airavata-django-portal/django_airavata/apps/api/proto_render.py
index 41b8e2d03..16b7afa70 100644
--- a/airavata-django-portal/django_airavata/apps/api/proto_render.py
+++ b/airavata-django-portal/django_airavata/apps/api/proto_render.py
@@ -33,15 +33,15 @@ from google.protobuf.message import Message
 try:  # pydantic is optional on this path; only composed shapes use it
     from pydantic import BaseModel as _PydanticBaseModel
 except Exception:  # pragma: no cover - pydantic always present in practice
-    _PydanticBaseModel = None
+    _PydanticBaseModel = None  # ty: ignore[invalid-assignment]  # optional-import fallback sentinel
 
 from airavata_sdk.helpers._envelope import WithAccess, WithGroupAccess
 
 # The single source of truth for how every proto on the read path is rendered.
-_MESSAGE_TO_DICT_OPTS = dict(
-    preserving_proto_field_name=True,
-    always_print_fields_with_no_presence=True,
-)
+_MESSAGE_TO_DICT_OPTS = {
+    "preserving_proto_field_name": True,
+    "always_print_fields_with_no_presence": True,
+}
 
 
 def proto_to_dict(message: Message) -> dict:
@@ -69,10 +69,10 @@ def to_jsonable(obj):
         base["is_owner"] = obj.is_owner
         base["is_member"] = obj.is_member
         base["is_gateway_admins_group"] = obj.is_gateway_admins_group
-        base["is_read_only_gateway_admins_group"] = \
+        base["is_read_only_gateway_admins_group"] = (
             obj.is_read_only_gateway_admins_group
-        base["is_default_gateway_users_group"] = \
-            obj.is_default_gateway_users_group
+        )
+        base["is_default_gateway_users_group"] = obj.is_default_gateway_users_group
         return base
     if isinstance(obj, (list, tuple)):
         return [to_jsonable(x) for x in obj]
@@ -83,8 +83,7 @@ def to_jsonable(obj):
         # model may carry protos / ``WithAccess`` envelopes / nested dicts that
         # the SAME renderer flattens.  Plain JSON-able fields are unchanged.
         return {
-            name: to_jsonable(getattr(obj, name))
-            for name in type(obj).model_fields
+            name: to_jsonable(getattr(obj, name)) for name in type(obj).model_fields
         }
     return obj
 
@@ -94,5 +93,4 @@ class ProtoJSONRenderer:
     JSON-encodes it to bytes. Page views call ``.render(data) -> bytes``."""
 
     def render(self, data, accepted_media_type=None, renderer_context=None):
-        return json.dumps(
-            to_jsonable(data), cls=DjangoJSONEncoder).encode('utf-8')
+        return json.dumps(to_jsonable(data), cls=DjangoJSONEncoder).encode("utf-8")
diff --git a/airavata-django-portal/django_airavata/apps/api/serializers.py b/airavata-django-portal/django_airavata/apps/api/serializers.py
index 39c301537..be9751d37 100644
--- a/airavata-django-portal/django_airavata/apps/api/serializers.py
+++ b/airavata-django-portal/django_airavata/apps/api/serializers.py
@@ -15,21 +15,23 @@ def user_has_access(request, resource_id, permission="WRITE"):
     return request.airavata.sharing.user_has_access(
         resource_id=resource_id,
         user_id=request.user.username,
-        permission_type=permission)
+        permission_type=permission,
+    )
 
 
 def _credential_store_pb2():
     from airavata_sdk.generated.org.apache.airavata.model.credential.store import (
         credential_store_pb2,
     )
+
     return credential_store_pb2
 
 
 def application_preferences_data(application_preferences):
     # Same field set the old ModelSerializer emitted (exclude id/username/fk).
     return {
-        'application_id': application_preferences.application_id,
-        'favorite': application_preferences.favorite,
+        "application_id": application_preferences.application_id,
+        "favorite": application_preferences.favorite,
     }
 
 
@@ -37,15 +39,13 @@ def workspace_preferences_data(workspace_preferences):
     # Read-only; matches the old ModelSerializer (exclude username, nest the
     # application_preferences child list).
     return {
-        'most_recent_project_id':
-            workspace_preferences.most_recent_project_id,
-        'most_recent_group_resource_profile_id':
-            workspace_preferences.most_recent_group_resource_profile_id,
-        'most_recent_compute_resource_id':
-            workspace_preferences.most_recent_compute_resource_id,
-        'application_preferences': [
+        "most_recent_project_id": workspace_preferences.most_recent_project_id,
+        "most_recent_group_resource_profile_id": workspace_preferences.most_recent_group_resource_profile_id,
+        "most_recent_compute_resource_id": workspace_preferences.most_recent_compute_resource_id,
+        "application_preferences": [
             application_preferences_data(ap)
-            for ap in workspace_preferences.applicationpreferences_set.all()],
+            for ap in workspace_preferences.applicationpreferences_set.all()
+        ],
     }
 
 
@@ -53,9 +53,17 @@ def workspace_preferences_data(workspace_preferences):
 # camelCase IAMUserProfile body is validated for its required field set; the
 # group-membership diff drives the add/remove the view applies.
 _IAM_USER_REQUIRED_FIELDS = (
-    'airavataInternalUserId', 'userId', 'gatewayId', 'email', 'firstName',
-    'lastName', 'enabled', 'emailVerified', 'airavataUserProfileExists',
-    'creationTime', 'groups',
+    "airavataInternalUserId",
+    "userId",
+    "gatewayId",
+    "email",
+    "firstName",
+    "lastName",
+    "enabled",
+    "emailVerified",
+    "airavataUserProfileExists",
+    "creationTime",
+    "groups",
 )
 
 
@@ -67,7 +75,7 @@ def validate_iam_user_body(data):
     errors = {}
     for field in _IAM_USER_REQUIRED_FIELDS:
         if data.get(field) is None:
-            errors[field] = ['This field is required.']
+            errors[field] = ["This field is required."]
     if errors:
         raise ValidationError(errors)
     return data
@@ -81,7 +89,7 @@ def iam_user_group_diff(existing_groups, data):
     the frontend sends). Returns ``(added_group_ids, removed_group_ids)``.
     """
     existing_group_ids = [group.id for group in existing_groups]
-    new_group_ids = [group['id'] for group in data.get('groups', [])]
+    new_group_ids = [group["id"] for group in data.get("groups", [])]
     added_group_ids = list(set(new_group_ids) - set(existing_group_ids))
     removed_group_ids = list(set(existing_group_ids) - set(new_group_ids))
     return added_group_ids, removed_group_ids
@@ -91,10 +99,10 @@ def parse_update_username(data):
     """Validate an update-username body (the full profile plus ``newUsername``),
     returning ``(user_id, new_username)``."""
     validated = validate_iam_user_body(data)
-    new_username = validated.get('newUsername')
+    new_username = validated.get("newUsername")
     if not new_username:
-        raise ValidationError({'newUsername': ['This field is required.']})
-    return validated['userId'], new_username
+        raise ValidationError({"newUsername": ["This field is required."]})
+    return validated["userId"], new_username
 
 
 class ValidationError(Exception):
@@ -109,9 +117,9 @@ class ValidationError(Exception):
 def settings_data(file_upload_max_file_size, tus_endpoint, pga_url):
     # camelCase keys preserved verbatim (frontend Settings.js contract).
     return {
-        'fileUploadMaxFileSize': file_upload_max_file_size,
-        'tusEndpoint': tus_endpoint,
-        'pgaUrl': pga_url,
+        "fileUploadMaxFileSize": file_upload_max_file_size,
+        "tusEndpoint": tus_endpoint,
+        "pgaUrl": pga_url,
     }
 
 
@@ -124,46 +132,50 @@ def parse_log_record(data):
     errors = {}
     if not isinstance(data, dict):
         raise ValidationError("Invalid log record.")
-    level = data.get('level')
-    message = data.get('message')
-    stacktrace = data.get('stacktrace')
+    level = data.get("level")
+    message = data.get("message")
+    stacktrace = data.get("stacktrace")
     if not level:
-        errors['level'] = ['This field is required.']
+        errors["level"] = ["This field is required."]
     if not message:
-        errors['message'] = ['This field is required.']
-    if 'details' not in data:
-        errors['details'] = ['This field is required.']
+        errors["message"] = ["This field is required."]
+    if "details" not in data:
+        errors["details"] = ["This field is required."]
     if stacktrace is None:
-        errors['stacktrace'] = ['This field is required.']
+        errors["stacktrace"] = ["This field is required."]
     elif not isinstance(stacktrace, list):
-        errors['stacktrace'] = ['Expected a list of items.']
+        errors["stacktrace"] = ["Expected a list of items."]
     if errors:
         raise ValidationError(errors)
     try:
-        details = json.dumps(data['details'])
-    except (TypeError, ValueError):
-        raise ValidationError({'details': ['Value must be valid JSON.']})
+        details = json.dumps(data["details"])
+    except (TypeError, ValueError) as err:
+        raise ValidationError({"details": ["Value must be valid JSON."]}) from err
     return {
-        'level': level,
-        'message': message,
-        'details': details,
-        'stacktrace': [str(s) for s in stacktrace],
+        "level": level,
+        "message": message,
+        "details": details,
+        "stacktrace": [str(s) for s in stacktrace],
     }
 
 
 def render_log_record(validated):
     # Mirror the old serializer's read path: details JSON-decoded, rest verbatim.
     try:
-        details = json.loads(validated['details']) if validated['details'] else validated['details']
+        details = (
+            json.loads(validated["details"])
+            if validated["details"]
+            else validated["details"]
+        )
     except Exception:
-        details = validated['details']
+        details = validated["details"]
     return {
-        'level': validated['level'],
-        'message': validated['message'],
-        'details': details,
-        'stacktrace': validated['stacktrace'],
+        "level": validated["level"],
+        "message": validated["message"],
+        "details": details,
+        "stacktrace": validated["stacktrace"],
     }
 
 
 def queue_settings_calculator_data(calculator):
-    return {'id': calculator.id, 'name': calculator.name}
+    return {"id": calculator.id, "name": calculator.name}
diff --git a/airavata-django-portal/django_airavata/apps/api/signals.py b/airavata-django-portal/django_airavata/apps/api/signals.py
index 62d2a4363..b23227595 100644
--- a/airavata-django-portal/django_airavata/apps/api/signals.py
+++ b/airavata-django-portal/django_airavata/apps/api/signals.py
@@ -21,8 +21,8 @@ def create_user_storage_dir(sender, request, user, **kwargs):
     storage = request.airavata.storage
     if not storage.dir_exists("~/"):
         storage.create_dir("~/")
-        log.info("Created home directory for user {}".format(user.username))
+        log.info(f"Created home directory for user {user.username}")
 
-    if hasattr(settings, 'GATEWAY_DATA_SHARED_DIRECTORIES'):
+    if hasattr(settings, "GATEWAY_DATA_SHARED_DIRECTORIES"):
         for name, entry in settings.GATEWAY_DATA_SHARED_DIRECTORIES.items():
-            storage.create_symlink(entry['path'], "~/" + name)
+            storage.create_symlink(entry["path"], "~/" + name)
diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_data_products_contract.py b/airavata-django-portal/django_airavata/apps/api/tests/test_data_products_contract.py
index bbefac881..2966b288d 100644
--- a/airavata-django-portal/django_airavata/apps/api/tests/test_data_products_contract.py
+++ b/airavata-django-portal/django_airavata/apps/api/tests/test_data_products_contract.py
@@ -17,13 +17,13 @@ decimal STRINGS (``product_size`` is int32 -> a JSON number), no camelCase keys,
 and the two access scalars merged on top.
 """
 
-from django.test import SimpleTestCase
-
 from airavata_sdk.generated.org.apache.airavata.model.data.replica import (
     replica_catalog_pb2 as rc,
 )
 from airavata_sdk.helpers._envelope import WithAccess
 from airavata_sdk.helpers.research_resources import get_data_product
+from django.test import SimpleTestCase
+
 from django_airavata.apps.api.proto_render import to_jsonable
 
 _PRODUCT_URI = "airavata-dp://gateway/alice/file.txt"
@@ -33,6 +33,7 @@ _PRODUCT_URI = "airavata-dp://gateway/alice/file.txt"
 # Stub client — returns a fixed proto from the raw research facade
 # ---------------------------------------------------------------------------
 
+
 class _FakeResearch:
     def __init__(self, product):
         self._product = product
@@ -121,10 +122,9 @@ _EXPECTED_SNAPSHOT = {
 
 
 class DataProductContractSnapshotTest(SimpleTestCase):
-
     def _render(self, has_write=True, username="alice"):
         client = _FakeClient(_make_product(), username=username)
-        result = get_data_product(client, _PRODUCT_URI, has_write=has_write)
+        result = get_data_product(client, _PRODUCT_URI, has_write=has_write)  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         return to_jsonable(result)
 
     # ------------------------------------------------------------------
@@ -134,7 +134,7 @@ class DataProductContractSnapshotTest(SimpleTestCase):
     def test_sdk_returns_withaccess_carrying_the_proto(self):
         product = _make_product()
         client = _FakeClient(product)
-        result = get_data_product(client, _PRODUCT_URI, has_write=True)
+        result = get_data_product(client, _PRODUCT_URI, has_write=True)  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertIsInstance(result, WithAccess)
         # the proto flows through wholesale — no field copied out.
         self.assertIs(result.message, product)
@@ -150,8 +150,7 @@ class DataProductContractSnapshotTest(SimpleTestCase):
         self.assertEqual(self._render(), _EXPECTED_SNAPSHOT)
 
     def test_exact_top_level_key_set(self):
-        self.assertEqual(
-            set(self._render().keys()), set(_EXPECTED_SNAPSHOT.keys()))
+        self.assertEqual(set(self._render().keys()), set(_EXPECTED_SNAPSHOT.keys()))
 
     def test_exact_replica_key_set(self):
         replica = self._render()["replica_locations"][0]
@@ -166,8 +165,8 @@ class DataProductContractSnapshotTest(SimpleTestCase):
             if isinstance(obj, dict):
                 for key, value in obj.items():
                     self.assertEqual(
-                        key, key.lower(),
-                        f"key {key!r} is not lowercase/snake_case")
+                        key, key.lower(), f"key {key!r} is not lowercase/snake_case"
+                    )
                     _check(value)
             elif isinstance(obj, list):
                 for item in obj:
@@ -178,19 +177,38 @@ class DataProductContractSnapshotTest(SimpleTestCase):
     def test_no_camelcase_keys(self):
         rendered = self._render()
         for legacy in (
-            "productUri", "productURI", "gatewayId", "gatewayID",
-            "parentProductUri", "productName", "productDescription",
-            "ownerName", "dataProductType", "productSize", "creationTime",
-            "lastModifiedTime", "productMetadata", "replicaLocations",
-            "userHasWriteAccess", "isOwner", "url",
+            "productUri",
+            "productURI",
+            "gatewayId",
+            "gatewayID",
+            "parentProductUri",
+            "productName",
+            "productDescription",
+            "ownerName",
+            "dataProductType",
+            "productSize",
+            "creationTime",
+            "lastModifiedTime",
+            "productMetadata",
+            "replicaLocations",
+            "userHasWriteAccess",
+            "isOwner",
+            "url",
         ):
             self.assertNotIn(legacy, rendered)
         replica = rendered["replica_locations"][0]
         for legacy in (
-            "replicaId", "productUri", "replicaName", "replicaDescription",
-            "creationTime", "lastModifiedTime", "validUntilTime",
-            "replicaLocationCategory", "replicaPersistentType",
-            "storageResourceId", "filePath",
+            "replicaId",
+            "productUri",
+            "replicaName",
+            "replicaDescription",
+            "creationTime",
+            "lastModifiedTime",
+            "validUntilTime",
+            "replicaLocationCategory",
+            "replicaPersistentType",
+            "storageResourceId",
+            "filePath",
         ):
             self.assertNotIn(legacy, replica)
 
@@ -203,17 +221,14 @@ class DataProductContractSnapshotTest(SimpleTestCase):
 
     def test_replica_enums_render_as_member_names(self):
         replica = self._render()["replica_locations"][0]
-        self.assertEqual(
-            replica["replica_location_category"], "GATEWAY_DATA_STORE")
+        self.assertEqual(replica["replica_location_category"], "GATEWAY_DATA_STORE")
         self.assertEqual(replica["replica_persistent_type"], "TRANSIENT")
 
     def test_unknown_type_renders_as_sentinel_name(self):
         product = rc.DataProductModel(product_uri=_PRODUCT_URI)
         client = _FakeClient(product)
-        rendered = to_jsonable(
-            get_data_product(client, _PRODUCT_URI, has_write=False))
-        self.assertEqual(
-            rendered["data_product_type"], "DATA_PRODUCT_TYPE_UNKNOWN")
+        rendered = to_jsonable(get_data_product(client, _PRODUCT_URI, has_write=False))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        self.assertEqual(rendered["data_product_type"], "DATA_PRODUCT_TYPE_UNKNOWN")
 
     # ------------------------------------------------------------------
     # int64 / metadata field handling
@@ -231,7 +246,8 @@ class DataProductContractSnapshotTest(SimpleTestCase):
 
     def test_product_metadata_is_native_object(self):
         self.assertEqual(
-            self._render()["product_metadata"], {"mime-type": "text/plain"})
+            self._render()["product_metadata"], {"mime-type": "text/plain"}
+        )
 
     # ------------------------------------------------------------------
     # Access flags
@@ -253,7 +269,8 @@ class DataProductContractSnapshotTest(SimpleTestCase):
         product = rc.DataProductModel(product_uri="airavata-dp://empty")
         client = _FakeClient(product)
         rendered = to_jsonable(
-            get_data_product(client, "airavata-dp://empty", has_write=False))
+            get_data_product(client, "airavata-dp://empty", has_write=False)  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        )
         self.assertEqual(rendered["replica_locations"], [])
         self.assertEqual(rendered["gateway_id"], "")
         self.assertEqual(rendered["product_size"], 0)
diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_full_experiment_contract.py b/airavata-django-portal/django_airavata/apps/api/tests/test_full_experiment_contract.py
index 4ae5ed6de..f6e5763af 100644
--- a/airavata-django-portal/django_airavata/apps/api/tests/test_full_experiment_contract.py
+++ b/airavata-django-portal/django_airavata/apps/api/tests/test_full_experiment_contract.py
@@ -27,15 +27,13 @@ keys only, enums as NAMES, timestamps as epoch-millis strings, NO camelCase, NO
 hyperlinks.
 """
 
-from django.test import SimpleTestCase
-
-from airavata_sdk.generated.org.apache.airavata.model.appcatalog.appdeployment import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.appcatalog.appdeployment import (
     app_deployment_pb2,
 )
-from airavata_sdk.generated.org.apache.airavata.model.appcatalog.appinterface import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.appcatalog.appinterface import (
     app_interface_pb2,
 )
-from airavata_sdk.generated.org.apache.airavata.model.appcatalog.computeresource import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.appcatalog.computeresource import (
     compute_resource_pb2,
 )
 from airavata_sdk.generated.org.apache.airavata.model.application.io import (
@@ -60,111 +58,186 @@ from airavata_sdk.generated.org.apache.airavata.model.workspace import (
     workspace_pb2,
 )
 from airavata_sdk.helpers.research_resources import get_full_experiment
-from django_airavata.apps.api.proto_render import to_jsonable
+from django.test import SimpleTestCase
 
+from django_airavata.apps.api.proto_render import to_jsonable
 
 # ---------------------------------------------------------------------------
 # Representative composite protos
 # ---------------------------------------------------------------------------
 
+
 def _make_experiment():
     job = job_pb2.JobModel(
-        job_id="job-1", task_id="task-1", process_id="proc-1", job_name="J",
+        job_id="job-1",
+        task_id="task-1",
+        process_id="proc-1",
+        job_name="J",
         creation_time=1705320000000,
-        job_statuses=[status_pb2.JobStatus(
-            job_state=status_pb2.JobState.ACTIVE,
-            time_of_state_change=1705320001000, reason="r", status_id="js-1")])
+        job_statuses=[
+            status_pb2.JobStatus(
+                job_state=status_pb2.JobState.ACTIVE,
+                time_of_state_change=1705320001000,
+                reason="r",
+                status_id="js-1",
+            )
+        ],
+    )
     task = task_pb2.TaskModel(
-        task_id="task-1", task_type=task_pb2.TaskTypes.JOB_SUBMISSION,
-        parent_process_id="proc-1", creation_time=1705320000000,
-        task_statuses=[status_pb2.TaskStatus(
-            state=status_pb2.TaskState.TASK_STATE_COMPLETED,
-            time_of_state_change=1705320002000, reason="ok",
-            status_id="ts-1")],
-        jobs=[job])
+        task_id="task-1",
+        task_type=task_pb2.TaskTypes.JOB_SUBMISSION,
+        parent_process_id="proc-1",
+        creation_time=1705320000000,
+        task_statuses=[
+            status_pb2.TaskStatus(
+                state=status_pb2.TaskState.TASK_STATE_COMPLETED,
+                time_of_state_change=1705320002000,
+                reason="ok",
+                status_id="ts-1",
+            )
+        ],
+        jobs=[job],
+    )
     process = process_pb2.ProcessModel(
-        process_id="proc-1", experiment_id="exp-contract-1",
+        process_id="proc-1",
+        experiment_id="exp-contract-1",
         creation_time=1705320000000,
-        process_statuses=[status_pb2.ProcessStatus(
-            state=status_pb2.ProcessState.PROCESS_STATE_COMPLETED,
-            time_of_state_change=1705320003000, reason="done",
-            status_id="ps-1", process_id="proc-1")],
-        tasks=[task])
+        process_statuses=[
+            status_pb2.ProcessStatus(
+                state=status_pb2.ProcessState.PROCESS_STATE_COMPLETED,
+                time_of_state_change=1705320003000,
+                reason="done",
+                status_id="ps-1",
+                process_id="proc-1",
+            )
+        ],
+        tasks=[task],
+    )
     ucd = experiment_pb2.UserConfigurationDataModel(
         airavata_auto_schedule=False,
         computational_resource_scheduling=(
             scheduling_pb2.ComputationalResourceSchedulingModel(
-                resource_host_id="comp-1", total_cpu_count=4, node_count=1,
-                queue_name="normal", wall_time_limit=30)))
+                resource_host_id="comp-1",
+                total_cpu_count=4,
+                node_count=1,
+                queue_name="normal",
+                wall_time_limit=30,
+            )
+        ),
+    )
     return experiment_pb2.ExperimentModel(
-        experiment_id="exp-contract-1", project_id="proj-contract-1",
+        experiment_id="exp-contract-1",
+        project_id="proj-contract-1",
         gateway_id="default",
         experiment_type=experiment_pb2.ExperimentType.SINGLE_APPLICATION,
-        user_name="alice", experiment_name="Contract Experiment",
-        creation_time=1705320000000, description="desc",
-        execution_id="iface-1", email_addresses=["a@x.org"],
+        user_name="alice",
+        experiment_name="Contract Experiment",
+        creation_time=1705320000000,
+        description="desc",
+        execution_id="iface-1",
+        email_addresses=["a@x.org"],
         user_configuration_data=ucd,
-        experiment_inputs=[io.InputDataObjectType(
-            name="in1", value="airavata-dp://in", type=io.DataType.URI,
-            input_order=1, is_required=True)],
-        experiment_outputs=[io.OutputDataObjectType(
-            name="out1", value="airavata-dp://out", type=io.DataType.URI)],
-        experiment_status=[status_pb2.ExperimentStatus(
-            state=status_pb2.ExperimentState.EXPERIMENT_STATE_EXECUTING,
-            time_of_state_change=1705320004000, reason="running",
-            status_id="es-1")],
-        processes=[process])
+        experiment_inputs=[
+            io.InputDataObjectType(
+                name="in1",
+                value="airavata-dp://in",
+                type=io.DataType.URI,
+                input_order=1,
+                is_required=True,
+            )
+        ],
+        experiment_outputs=[
+            io.OutputDataObjectType(
+                name="out1", value="airavata-dp://out", type=io.DataType.URI
+            )
+        ],
+        experiment_status=[
+            status_pb2.ExperimentStatus(
+                state=status_pb2.ExperimentState.EXPERIMENT_STATE_EXECUTING,
+                time_of_state_change=1705320004000,
+                reason="running",
+                status_id="es-1",
+            )
+        ],
+        processes=[process],
+    )
 
 
 def _make_project():
     return workspace_pb2.Project(
-        project_id="proj-contract-1", owner="alice", gateway_id="default",
-        name="Contract Project", description="p", creation_time=1705320000000)
+        project_id="proj-contract-1",
+        owner="alice",
+        gateway_id="default",
+        name="Contract Project",
+        description="p",
+        creation_time=1705320000000,
+    )
 
 
 def _make_app_interface():
     return app_interface_pb2.ApplicationInterfaceDescription(
-        application_interface_id="iface-1", application_name="App",
-        application_modules=["mod-1"])
+        application_interface_id="iface-1",
+        application_name="App",
+        application_modules=["mod-1"],
+    )
 
 
 def _make_app_module():
     return app_deployment_pb2.ApplicationModule(
-        app_module_id="mod-1", app_module_name="mod",
-        app_module_version="1.0", app_module_description="m")
+        app_module_id="mod-1",
+        app_module_name="mod",
+        app_module_version="1.0",
+        app_module_description="m",
+    )
 
 
 def _make_compute_resource():
     return compute_resource_pb2.ComputeResourceDescription(
-        compute_resource_id="comp-1", host_name="hpc.example.org",
-        resource_description="HPC", enabled=True)
+        compute_resource_id="comp-1",
+        host_name="hpc.example.org",
+        resource_description="HPC",
+        enabled=True,
+    )
 
 
 def _make_data_product(uri, owner_name):
     return rc.DataProductModel(
-        product_uri=uri, gateway_id="default", owner_name=owner_name,
+        product_uri=uri,
+        gateway_id="default",
+        owner_name=owner_name,
         product_name=uri.split("://")[-1],
         data_product_type=rc.DataProductType.FILE,
-        creation_time=1705320000000, last_modified_time=1705320005000,
-        replica_locations=[rc.DataReplicaLocationModel(
-            replica_name="gw copy",
-            replica_location_category=(
-                rc.ReplicaLocationCategory.GATEWAY_DATA_STORE),
-            replica_persistent_type=rc.ReplicaPersistentType.TRANSIENT,
-            storage_resource_id="store-1",
-            file_path="/data/" + uri.split("://")[-1])])
+        creation_time=1705320000000,
+        last_modified_time=1705320005000,
+        replica_locations=[
+            rc.DataReplicaLocationModel(
+                replica_name="gw copy",
+                replica_location_category=(
+                    rc.ReplicaLocationCategory.GATEWAY_DATA_STORE
+                ),
+                replica_persistent_type=rc.ReplicaPersistentType.TRANSIENT,
+                storage_resource_id="store-1",
+                file_path="/data/" + uri.split("://")[-1],
+            )
+        ],
+    )
 
 
 def _make_job():
     return job_pb2.JobModel(
-        job_id="job-1", task_id="task-1", process_id="proc-1", job_name="J",
-        creation_time=1705320000000)
+        job_id="job-1",
+        task_id="task-1",
+        process_id="proc-1",
+        job_name="J",
+        creation_time=1705320000000,
+    )
 
 
 # ---------------------------------------------------------------------------
 # Stub client
 # ---------------------------------------------------------------------------
 
+
 class _FakeResearch:
     def __init__(self):
         self._experiment = _make_experiment()
@@ -229,14 +302,20 @@ def _output_views(_experiment, _app_interface):
 
 
 class FullExperimentContractSnapshotTest(SimpleTestCase):
-
-    def _render(self, *, username="alice", sharing_has_access=True,
-                project_has_read=True, module_has_write=True,
-                dp_write=_dp_write, output_views=_output_views):
-        client = _FakeClient(
-            username=username, sharing_has_access=sharing_has_access)
+    def _render(
+        self,
+        *,
+        username="alice",
+        sharing_has_access=True,
+        project_has_read=True,
+        module_has_write=True,
+        dp_write=_dp_write,
+        output_views=_output_views,
+    ):
+        client = _FakeClient(username=username, sharing_has_access=sharing_has_access)
         result = get_full_experiment(
-            client, "exp-contract-1",
+            client,  # ty: ignore[invalid-argument-type]  # _FakeClient duck-types AiravataClient for this contract test
+            "exp-contract-1",
             project_has_read=project_has_read,
             module_has_write=module_has_write,
             data_product_write_fn=dp_write,
@@ -250,18 +329,26 @@ class FullExperimentContractSnapshotTest(SimpleTestCase):
 
     def test_top_level_key_set(self):
         rendered = self._render()
-        self.assertEqual(set(rendered.keys()), {
-            "experiment_id", "experiment", "project", "application_module",
-            "compute_resource", "input_data_products", "output_data_products",
-            "job_details", "output_views",
-        })
+        self.assertEqual(
+            set(rendered.keys()),
+            {
+                "experiment_id",
+                "experiment",
+                "project",
+                "application_module",
+                "compute_resource",
+                "input_data_products",
+                "output_data_products",
+                "job_details",
+                "output_views",
+            },
+        )
 
     def test_top_level_experiment_id(self):
         self.assertEqual(self._render()["experiment_id"], "exp-contract-1")
 
     def test_output_views_passed_through(self):
-        self.assertEqual(
-            self._render()["output_views"], {"out1": ["view-provider-a"]})
+        self.assertEqual(self._render()["output_views"], {"out1": ["view-provider-a"]})
 
     # ------------------------------------------------------------------
     # Nested experiment (WithAccess[ExperimentModel] flattened)
@@ -280,8 +367,8 @@ class FullExperimentContractSnapshotTest(SimpleTestCase):
         exp = self._render()["experiment"]
         self.assertEqual(exp["experiment_type"], "SINGLE_APPLICATION")
         self.assertEqual(
-            exp["experiment_status"][0]["state"],
-            "EXPERIMENT_STATE_EXECUTING")
+            exp["experiment_status"][0]["state"], "EXPERIMENT_STATE_EXECUTING"
+        )
 
     def test_experiment_timestamp_is_epoch_millis_string(self):
         exp = self._render()["experiment"]
@@ -294,8 +381,7 @@ class FullExperimentContractSnapshotTest(SimpleTestCase):
         self.assertEqual(proc["process_id"], "proc-1")
         task = proc["tasks"][0]
         self.assertEqual(task["task_type"], "JOB_SUBMISSION")
-        self.assertEqual(
-            task["task_statuses"][0]["state"], "TASK_STATE_COMPLETED")
+        self.assertEqual(task["task_statuses"][0]["state"], "TASK_STATE_COMPLETED")
         job = task["jobs"][0]
         self.assertEqual(job["job_id"], "job-1")
         self.assertEqual(job["job_statuses"][0]["job_state"], "ACTIVE")
@@ -351,10 +437,9 @@ class FullExperimentContractSnapshotTest(SimpleTestCase):
         ins = rendered["input_data_products"]
         outs = rendered["output_data_products"]
         self.assertEqual([d["product_uri"] for d in ins], ["airavata-dp://in"])
-        self.assertEqual(
-            [d["product_uri"] for d in outs], ["airavata-dp://out"])
+        self.assertEqual([d["product_uri"] for d in outs], ["airavata-dp://out"])
         # is_owner SDK-trivial (owner_name == username "alice").
-        self.assertTrue(ins[0]["is_owner"])    # owner_name="alice"
+        self.assertTrue(ins[0]["is_owner"])  # owner_name="alice"
         self.assertFalse(outs[0]["is_owner"])  # owner_name="bob"
         # write flag is the resolver result (stub returns True).
         self.assertTrue(ins[0]["user_has_write_access"])
@@ -375,8 +460,12 @@ class FullExperimentContractSnapshotTest(SimpleTestCase):
     def test_data_product_dropped_legacy_keys_absent(self):
         dp = self._render()["input_data_products"][0]
         # The proto-direct data-product contract dropped these legacy aliases.
-        for legacy in ("download_url", "is_input_file_upload", "modified_time",
-                       "filesize"):
+        for legacy in (
+            "download_url",
+            "is_input_file_upload",
+            "modified_time",
+            "filesize",
+        ):
             self.assertNotIn(legacy, dp)
 
     # ------------------------------------------------------------------
@@ -407,22 +496,35 @@ class FullExperimentContractSnapshotTest(SimpleTestCase):
     def test_no_camelcase_keys_anywhere(self):
         for key in self._all_keys(self._render()):
             self.assertEqual(
-                key, key.lower(),
-                f"key {key!r} is not lowercase/snake_case")
+                key, key.lower(), f"key {key!r} is not lowercase/snake_case"
+            )
 
     def test_no_hyperlink_keys_anywhere(self):
         keys = set(self._all_keys(self._render()))
-        for hyperlink in ("url", "full_experiment", "shared_entity",
-                          "experiments", "application_interface",
-                          "application_deployments"):
+        for hyperlink in (
+            "url",
+            "full_experiment",
+            "shared_entity",
+            "experiments",
+            "application_interface",
+            "application_deployments",
+        ):
             self.assertNotIn(hyperlink, keys)
 
     def test_no_legacy_camel_keys(self):
         keys = set(self._all_keys(self._render()))
-        for legacy in ("experimentId", "projectId", "creationTime",
-                       "userHasWriteAccess", "isOwner", "downloadURL",
-                       "computeResource", "applicationModule",
-                       "inputDataProducts", "outputViews"):
+        for legacy in (
+            "experimentId",
+            "projectId",
+            "creationTime",
+            "userHasWriteAccess",
+            "isOwner",
+            "downloadURL",
+            "computeResource",
+            "applicationModule",
+            "inputDataProducts",
+            "outputViews",
+        ):
             self.assertNotIn(legacy, keys)
 
     # ------------------------------------------------------------------
@@ -432,9 +534,13 @@ class FullExperimentContractSnapshotTest(SimpleTestCase):
     def test_chained_sharing_calls_use_write(self):
         client = _FakeClient()
         get_full_experiment(
-            client, "exp-contract-1",
-            project_has_read=True, module_has_write=True,
-            data_product_write_fn=_dp_write, output_views_fn=_output_views)
+            client,  # ty: ignore[invalid-argument-type]  # _FakeClient duck-types AiravataClient for this contract test
+            "exp-contract-1",
+            project_has_read=True,
+            module_has_write=True,
+            data_product_write_fn=_dp_write,
+            output_views_fn=_output_views,
+        )
         # One WRITE lookup for the experiment, one for the project.
         perms = [c[2] for c in client.sharing.calls]
         self.assertEqual(perms, ["WRITE", "WRITE"])
diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_group_resource_profiles_contract.py b/airavata-django-portal/django_airavata/apps/api/tests/test_group_resource_profiles_contract.py
index 2b1369be1..4ed81f3bb 100644
--- a/airavata-django-portal/django_airavata/apps/api/tests/test_group_resource_profiles_contract.py
+++ b/airavata-django-portal/django_airavata/apps/api/tests/test_group_resource_profiles_contract.py
@@ -31,20 +31,20 @@ composite write flag is resolved by the ViewSet and passed straight into the SDK
 function (no sharing lookup to stub).
 """
 
-from django.test import SimpleTestCase
-
-from airavata_sdk.generated.org.apache.airavata.model.appcatalog.groupresourceprofile import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.appcatalog.groupresourceprofile import (
     group_resource_profile_pb2 as grp,
 )
 from airavata_sdk.helpers._envelope import WithAccess
 from airavata_sdk.helpers.compute_resources import get_group_resource_profile
-from django_airavata.apps.api.proto_render import to_jsonable
+from django.test import SimpleTestCase
 
+from django_airavata.apps.api.proto_render import to_jsonable
 
 # ---------------------------------------------------------------------------
 # Stub client — returns a fixed proto from the raw compute facade
 # ---------------------------------------------------------------------------
 
+
 class _FakeCompute:
     def __init__(self, profile):
         self._profile = profile
@@ -69,11 +69,17 @@ def _make_slurm_pref():
         ssh_account_provisioner="prov",
         group_ssh_account_provisioner_configs=[
             grp.GroupAccountSSHProvisionerConfig(
-                resource_id="r1", config_name="cn", config_value="cv")],
+                resource_id="r1", config_name="cn", config_value="cv"
+            )
+        ],
         reservations=[
             grp.ComputeResourceReservation(
-                reservation_id="rid", queue_names=["q1"],
-                start_time=1705320000000, end_time=0)],
+                reservation_id="rid",
+                queue_names=["q1"],
+                start_time=1705320000000,
+                end_time=0,
+            )
+        ],
     )
     return grp.GroupComputeResourcePreference(
         compute_resource_id="cr-slurm",
@@ -82,8 +88,8 @@ def _make_slurm_pref():
         login_user_name="alice",
         scratch_location="/scratch",
         # proto SSH / SFTP — rendered as member NAMES, not Thrift ints.
-        preferred_job_submission_protocol=2,   # SSH
-        preferred_data_movement_protocol=3,    # SFTP
+        preferred_job_submission_protocol=2,  # SSH
+        preferred_data_movement_protocol=3,  # SFTP
         resource_type=grp.ResourceType.SLURM,
         specific_preferences=grp.EnvironmentSpecificPreferences(slurm=slurm),
     )
@@ -95,8 +101,11 @@ def _make_aws_pref():
         resource_type=grp.ResourceType.AWS,
         specific_preferences=grp.EnvironmentSpecificPreferences(
             aws=grp.AwsComputeResourcePreference(
-                region="us-east-1", preferred_ami_id="ami-1",
-                preferred_instance_type="t2.micro")),
+                region="us-east-1",
+                preferred_ami_id="ami-1",
+                preferred_instance_type="t2.micro",
+            )
+        ),
     )
 
 
@@ -111,16 +120,27 @@ def _make_profile():
         group_resource_profile_id="grp-1",
         group_resource_profile_name="Test GRP",
         compute_preferences=[
-            _make_slurm_pref(), _make_aws_pref(), _make_unset_oneof_pref()],
+            _make_slurm_pref(),
+            _make_aws_pref(),
+            _make_unset_oneof_pref(),
+        ],
         compute_resource_policies=[
             grp.ComputeResourcePolicy(
-                resource_policy_id="rp-1", compute_resource_id="cr-slurm",
-                allowed_batch_queues=["normal", "long"])],
+                resource_policy_id="rp-1",
+                compute_resource_id="cr-slurm",
+                allowed_batch_queues=["normal", "long"],
+            )
+        ],
         batch_queue_resource_policies=[
             grp.BatchQueueResourcePolicy(
-                resource_policy_id="bq-1", compute_resource_id="cr-slurm",
-                queuename="normal", max_allowed_nodes=10,
-                max_allowed_cores=100, max_allowed_walltime=60)],
+                resource_policy_id="bq-1",
+                compute_resource_id="cr-slurm",
+                queuename="normal",
+                max_allowed_nodes=10,
+                max_allowed_cores=100,
+                max_allowed_walltime=60,
+            )
+        ],
         creation_time=1705320000000,
         updated_time=1705323600000,
         default_credential_store_token="def-tok",
@@ -211,7 +231,10 @@ _EXPECTED_SNAPSHOT = {
     "group_resource_profile_id": "grp-1",
     "group_resource_profile_name": "Test GRP",
     "compute_preferences": [
-        _EXPECTED_SLURM_PREF, _EXPECTED_AWS_PREF, _EXPECTED_UNSET_PREF],
+        _EXPECTED_SLURM_PREF,
+        _EXPECTED_AWS_PREF,
+        _EXPECTED_UNSET_PREF,
+    ],
     "compute_resource_policies": [
         {
             "resource_policy_id": "rp-1",
@@ -243,11 +266,9 @@ _EXPECTED_SNAPSHOT = {
 
 
 class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
-
     def _render(self, has_write=True):
         client = _FakeClient(_make_profile())
-        result = get_group_resource_profile(
-            client, "grp-1", has_write=has_write)
+        result = get_group_resource_profile(client, "grp-1", has_write=has_write)  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         return to_jsonable(result)
 
     # ------------------------------------------------------------------
@@ -257,7 +278,7 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
     def test_sdk_returns_withaccess_carrying_the_proto(self):
         profile = _make_profile()
         client = _FakeClient(profile)
-        result = get_group_resource_profile(client, "grp-1", has_write=True)
+        result = get_group_resource_profile(client, "grp-1", has_write=True)  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertIsInstance(result, WithAccess)
         # the proto flows through wholesale — no field copied out.
         self.assertIs(result.message, profile)
@@ -273,8 +294,7 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
         self.assertEqual(self._render(), _EXPECTED_SNAPSHOT)
 
     def test_exact_top_level_key_set(self):
-        self.assertEqual(
-            set(self._render().keys()), set(_EXPECTED_SNAPSHOT.keys()))
+        self.assertEqual(set(self._render().keys()), set(_EXPECTED_SNAPSHOT.keys()))
 
     def test_exact_slurm_pref_key_set(self):
         cp = self._render()["compute_preferences"][0]
@@ -289,11 +309,11 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
             if isinstance(obj, dict):
                 for key, value in obj.items():
                     self.assertEqual(
-                        key, key.lower(),
-                        f"key {key!r} is not lowercase/snake_case")
+                        key, key.lower(), f"key {key!r} is not lowercase/snake_case"
+                    )
                     self.assertNotIn(
-                        "ID", key,
-                        f"key {key!r} carries a camelCase 'ID' fragment")
+                        "ID", key, f"key {key!r} carries a camelCase 'ID' fragment"
+                    )
                     _check(value)
             elif isinstance(obj, list):
                 for item in obj:
@@ -305,18 +325,31 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
         rendered = self._render()
         cp = rendered["compute_preferences"][0]
         for legacy in (
-            "gatewayId", "gatewayID", "groupResourceProfileId",
-            "groupResourceProfileName", "computePreferences",
-            "computeResourcePolicies", "batchQueueResourcePolicies",
-            "creationTime", "updatedTime", "defaultCredentialStoreToken",
-            "userHasWriteAccess", "isOwner", "url",
+            "gatewayId",
+            "gatewayID",
+            "groupResourceProfileId",
+            "groupResourceProfileName",
+            "computePreferences",
+            "computeResourcePolicies",
+            "batchQueueResourcePolicies",
+            "creationTime",
+            "updatedTime",
+            "defaultCredentialStoreToken",
+            "userHasWriteAccess",
+            "isOwner",
+            "url",
         ):
             self.assertNotIn(legacy, rendered)
         for legacy in (
-            "computeResourceId", "overridebyAiravata", "overrideByAiravata",
-            "loginUserName", "scratchLocation",
-            "preferredJobSubmissionProtocol", "preferredDataMovementProtocol",
-            "resourceType", "specificPreferences",
+            "computeResourceId",
+            "overridebyAiravata",
+            "overrideByAiravata",
+            "loginUserName",
+            "scratchLocation",
+            "preferredJobSubmissionProtocol",
+            "preferredDataMovementProtocol",
+            "resourceType",
+            "specificPreferences",
             "resourceSpecificCredentialStoreToken",
         ):
             self.assertNotIn(legacy, cp)
@@ -337,11 +370,11 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
     def test_unknown_enums_render_as_sentinel_names(self):
         cp = self._render()["compute_preferences"][2]
         self.assertEqual(
-            cp["preferred_job_submission_protocol"],
-            "JOB_SUBMISSION_PROTOCOL_UNKNOWN")
+            cp["preferred_job_submission_protocol"], "JOB_SUBMISSION_PROTOCOL_UNKNOWN"
+        )
         self.assertEqual(
-            cp["preferred_data_movement_protocol"],
-            "DATA_MOVEMENT_PROTOCOL_UNKNOWN")
+            cp["preferred_data_movement_protocol"], "DATA_MOVEMENT_PROTOCOL_UNKNOWN"
+        )
         self.assertEqual(cp["resource_type"], "RESOURCE_TYPE_UNKNOWN")
 
     # ------------------------------------------------------------------
@@ -353,8 +386,7 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
         sp = cp["specific_preferences"]
         # only the active branch is present — no legacy {"slurm", "aws"} mirror.
         self.assertEqual(set(sp.keys()), {"slurm"})
-        self.assertEqual(
-            sp["slurm"]["allocation_project_number"], "alloc-1")
+        self.assertEqual(sp["slurm"]["allocation_project_number"], "alloc-1")
         # the SLURM allocation_project_number is NOT flattened to the top level.
         self.assertNotIn("allocation_project_number", cp)
 
@@ -370,8 +402,9 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
         self.assertNotIn("specific_preferences", cp)
 
     def test_slurm_provisioner_acronym_key_is_proto_snake_case(self):
-        slurm = self._render()["compute_preferences"][0][
-            "specific_preferences"]["slurm"]
+        slurm = self._render()["compute_preferences"][0]["specific_preferences"][
+            "slurm"
+        ]
         # proto-direct emits the plain snake_case proto field name, NOT the
         # legacy upper-acronym ``groupSSHAccountProvisionerConfigs``.
         self.assertIn("group_ssh_account_provisioner_configs", slurm)
@@ -392,25 +425,29 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
         self.assertEqual(rendered["creation_time"], "1705320000000")
         self.assertEqual(rendered["updated_time"], "1705323600000")
         # reservation 0-end_time stays "0" (always_print + int64->string).
-        res = rendered["compute_preferences"][0][
-            "specific_preferences"]["slurm"]["reservations"][0]
+        res = rendered["compute_preferences"][0]["specific_preferences"]["slurm"][
+            "reservations"
+        ][0]
         self.assertEqual(res["start_time"], "1705320000000")
         self.assertEqual(res["end_time"], "0")
 
     def test_default_token_passes_through_not_nulled(self):
         # the legacy empty-string -> None coercion is dropped.
-        self.assertEqual(
-            self._render()["default_credential_store_token"], "def-tok")
+        self.assertEqual(self._render()["default_credential_store_token"], "def-tok")
 
     def test_batch_queue_zero_int_preserved(self):
         profile = grp.GroupResourceProfile(
             group_resource_profile_id="grp-1",
             batch_queue_resource_policies=[
                 grp.BatchQueueResourcePolicy(
-                    resource_policy_id="bq-0", max_allowed_nodes=0)])
+                    resource_policy_id="bq-0", max_allowed_nodes=0
+                )
+            ],
+        )
         client = _FakeClient(profile)
         rendered = to_jsonable(
-            get_group_resource_profile(client, "grp-1", has_write=False))
+            get_group_resource_profile(client, "grp-1", has_write=False)  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        )
         bqp = rendered["batch_queue_resource_policies"][0]
         self.assertEqual(bqp["max_allowed_nodes"], 0)
 
@@ -433,7 +470,8 @@ class GroupResourceProfileContractSnapshotTest(SimpleTestCase):
         profile = grp.GroupResourceProfile(group_resource_profile_id="grp-empty")
         client = _FakeClient(profile)
         rendered = to_jsonable(
-            get_group_resource_profile(client, "grp-empty", has_write=False))
+            get_group_resource_profile(client, "grp-empty", has_write=False)  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        )
         self.assertEqual(rendered["compute_preferences"], [])
         self.assertEqual(rendered["compute_resource_policies"], [])
         self.assertEqual(rendered["batch_queue_resource_policies"], [])
diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_per_protocol_job_submission_contract.py b/airavata-django-portal/django_airavata/apps/api/tests/test_per_protocol_job_submission_contract.py
index c02b98526..32bd5d45b 100644
--- a/airavata-django-portal/django_airavata/apps/api/tests/test_per_protocol_job_submission_contract.py
+++ b/airavata-django-portal/django_airavata/apps/api/tests/test_per_protocol_job_submission_contract.py
@@ -21,9 +21,7 @@ verbatim by the proto int32 key (rendered as a decimal STRING), and NO camelCase
 hyperlinks.
 """
 
-from django.test import SimpleTestCase
-
-from airavata_sdk.generated.org.apache.airavata.model.appcatalog.computeresource import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.appcatalog.computeresource import (
     compute_resource_pb2,
 )
 from airavata_sdk.generated.org.apache.airavata.model.data.movement import (
@@ -38,13 +36,15 @@ from airavata_sdk.helpers.compute_resources import (
     get_ssh_job_submission,
     get_unicore_job_submission,
 )
-from django_airavata.apps.api.proto_render import to_jsonable
+from django.test import SimpleTestCase
 
+from django_airavata.apps.api.proto_render import to_jsonable
 
 # ---------------------------------------------------------------------------
 # Stub client — records the facade call, returns a fixed proto
 # ---------------------------------------------------------------------------
 
+
 class _FakeCompute:
     def __init__(self, *, local=None, ssh=None, unicore=None, cloud=None):
         self._local = local
@@ -79,6 +79,7 @@ class _FakeClient:
 # Representative protos
 # ---------------------------------------------------------------------------
 
+
 def _sec(name):
     return data_movement_pb2.SecurityProtocol.Value(name)
 
@@ -88,13 +89,15 @@ def _make_resource_job_manager():
         resource_job_manager_id="rjm-1",
         resource_job_manager_type=compute_resource_pb2.ResourceJobManagerType.SLURM,
         push_monitoring_endpoint="ep",
-        job_manager_bin_path="/bin")
-    rjm.job_manager_commands[
-        compute_resource_pb2.JobManagerCommand.SUBMISSION] = "sbatch"
-    rjm.job_manager_commands[
-        compute_resource_pb2.JobManagerCommand.JOB_MONITORING] = "squeue"
-    rjm.parallelism_prefix[
-        parallelism_pb2.ApplicationParallelismType.MPI] = "mpirun"
+        job_manager_bin_path="/bin",
+    )
+    rjm.job_manager_commands[compute_resource_pb2.JobManagerCommand.SUBMISSION] = (
+        "sbatch"
+    )
+    rjm.job_manager_commands[compute_resource_pb2.JobManagerCommand.JOB_MONITORING] = (
+        "squeue"
+    )
+    rjm.parallelism_prefix[parallelism_pb2.ApplicationParallelismType.MPI] = "mpirun"
     return rjm
 
 
@@ -102,7 +105,8 @@ def _make_local():
     return compute_resource_pb2.LOCALSubmission(
         job_submission_interface_id="ls-1",
         resource_job_manager=_make_resource_job_manager(),
-        security_protocol=_sec("LOCAL"))
+        security_protocol=_sec("LOCAL"),
+    )
 
 
 def _make_ssh():
@@ -113,7 +117,8 @@ def _make_ssh():
         alternative_ssh_host_name="alt.host",
         ssh_port=22,
         monitor_mode=compute_resource_pb2.MonitorMode.MONITOR_FORK,
-        batch_queue_email_senders=["a@x.com", "b@x.com"])
+        batch_queue_email_senders=["a@x.com", "b@x.com"],
+    )
 
 
 def _make_cloud():
@@ -123,14 +128,16 @@ def _make_cloud():
         node_id="n1",
         executable_type="exe",
         provider_name=compute_resource_pb2.ProviderName.AWSEC2,
-        user_account_name="acct")
+        user_account_name="acct",
+    )
 
 
 def _make_unicore():
     return compute_resource_pb2.UnicoreJobSubmission(
         job_submission_interface_id="u-1",
         security_protocol=_sec("GSI"),
-        unicore_end_point_url="https://u")
+        unicore_end_point_url="https://u",
+    )
 
 
 # ---------------------------------------------------------------------------
@@ -183,32 +190,31 @@ _EXPECTED_UNICORE = {
 
 
 class JobSubmissionContractSnapshotTest(SimpleTestCase):
-
     # ------------------------------------------------------------------
     # Full snapshots — SDK helper returns the proto, to_jsonable flattens it
     # ------------------------------------------------------------------
 
     def test_local_snapshot_matches(self):
         client = _FakeClient(local=_make_local())
-        rendered = to_jsonable(get_local_job_submission(client, "ls-1"))
+        rendered = to_jsonable(get_local_job_submission(client, "ls-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertEqual(rendered, _EXPECTED_LOCAL)
         self.assertEqual(client.compute.calls, [("local", "ls-1")])
 
     def test_ssh_snapshot_matches(self):
         client = _FakeClient(ssh=_make_ssh())
-        rendered = to_jsonable(get_ssh_job_submission(client, "ssh-1"))
+        rendered = to_jsonable(get_ssh_job_submission(client, "ssh-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertEqual(rendered, _EXPECTED_SSH)
         self.assertEqual(client.compute.calls, [("ssh", "ssh-1")])
 
     def test_cloud_snapshot_matches(self):
         client = _FakeClient(cloud=_make_cloud())
-        rendered = to_jsonable(get_cloud_job_submission(client, "cl-1"))
+        rendered = to_jsonable(get_cloud_job_submission(client, "cl-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertEqual(rendered, _EXPECTED_CLOUD)
         self.assertEqual(client.compute.calls, [("cloud", "cl-1")])
 
     def test_unicore_snapshot_matches(self):
         client = _FakeClient(unicore=_make_unicore())
-        rendered = to_jsonable(get_unicore_job_submission(client, "u-1"))
+        rendered = to_jsonable(get_unicore_job_submission(client, "u-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertEqual(rendered, _EXPECTED_UNICORE)
         self.assertEqual(client.compute.calls, [("unicore", "u-1")])
 
@@ -217,18 +223,22 @@ class JobSubmissionContractSnapshotTest(SimpleTestCase):
     # ------------------------------------------------------------------
 
     def test_enums_are_member_names_not_thrift_ints(self):
-        ssh = to_jsonable(get_ssh_job_submission(
-            _FakeClient(ssh=_make_ssh()), "ssh-1"))
+        ssh = to_jsonable(get_ssh_job_submission(_FakeClient(ssh=_make_ssh()), "ssh-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertEqual(ssh["security_protocol"], "SSH_KEYS")
         self.assertEqual(ssh["monitor_mode"], "MONITOR_FORK")
         self.assertEqual(
-            ssh["resource_job_manager"]["resource_job_manager_type"], "SLURM")
-        cloud = to_jsonable(get_cloud_job_submission(
-            _FakeClient(cloud=_make_cloud()), "cl-1"))
+            ssh["resource_job_manager"]["resource_job_manager_type"], "SLURM"
+        )
+        cloud = to_jsonable(
+            get_cloud_job_submission(_FakeClient(cloud=_make_cloud()), "cl-1")  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        )
         self.assertEqual(cloud["provider_name"], "AWSEC2")
         # Not the historical Thrift integers (1 / 4 / 2 / 1).
-        for v in (ssh["security_protocol"], ssh["monitor_mode"],
-                  cloud["provider_name"]):
+        for v in (
+            ssh["security_protocol"],
+            ssh["monitor_mode"],
+            cloud["provider_name"],
+        ):
             self.assertIsInstance(v, str)
 
     # ------------------------------------------------------------------
@@ -236,13 +246,11 @@ class JobSubmissionContractSnapshotTest(SimpleTestCase):
     # ------------------------------------------------------------------
 
     def test_resource_job_manager_maps_keyed_verbatim(self):
-        ssh = to_jsonable(get_ssh_job_submission(
-            _FakeClient(ssh=_make_ssh()), "ssh-1"))
+        ssh = to_jsonable(get_ssh_job_submission(_FakeClient(ssh=_make_ssh()), "ssh-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         rjm = ssh["resource_job_manager"]
         # NOT the legacy composite "JobManagerCommand.SUBMISSION" keys — the
         # proto int32 enum value verbatim ("1" SUBMISSION, "2" JOB_MONITORING).
-        self.assertEqual(
-            rjm["job_manager_commands"], {"1": "sbatch", "2": "squeue"})
+        self.assertEqual(rjm["job_manager_commands"], {"1": "sbatch", "2": "squeue"})
         self.assertEqual(rjm["parallelism_prefix"], {"2": "mpirun"})
 
     # ------------------------------------------------------------------
@@ -250,8 +258,7 @@ class JobSubmissionContractSnapshotTest(SimpleTestCase):
     # ------------------------------------------------------------------
 
     def test_ssh_port_is_a_number(self):
-        ssh = to_jsonable(get_ssh_job_submission(
-            _FakeClient(ssh=_make_ssh()), "ssh-1"))
+        ssh = to_jsonable(get_ssh_job_submission(_FakeClient(ssh=_make_ssh()), "ssh-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertIsInstance(ssh["ssh_port"], int)
         self.assertEqual(ssh["ssh_port"], 22)
 
@@ -259,9 +266,11 @@ class JobSubmissionContractSnapshotTest(SimpleTestCase):
         """An unset message field (explicit presence) stays ABSENT — the nested
         resource_job_manager is only present when populated."""
         local = compute_resource_pb2.LOCALSubmission(
-            job_submission_interface_id="ls-min")
-        rendered = to_jsonable(get_local_job_submission(
-            _FakeClient(local=local), "ls-min"))
+            job_submission_interface_id="ls-min"
+        )
+        rendered = to_jsonable(
+            get_local_job_submission(_FakeClient(local=local), "ls-min")  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        )
         self.assertNotIn("resource_job_manager", rendered)
 
     # ------------------------------------------------------------------
@@ -280,25 +289,30 @@ class JobSubmissionContractSnapshotTest(SimpleTestCase):
         return keys
 
     def test_no_camelcase_or_acronym_keys(self):
-        ssh = to_jsonable(get_ssh_job_submission(
-            _FakeClient(ssh=_make_ssh()), "ssh-1"))
-        uni = to_jsonable(get_unicore_job_submission(
-            _FakeClient(unicore=_make_unicore()), "u-1"))
+        ssh = to_jsonable(get_ssh_job_submission(_FakeClient(ssh=_make_ssh()), "ssh-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        uni = to_jsonable(
+            get_unicore_job_submission(_FakeClient(unicore=_make_unicore()), "u-1")  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
+        )
         legacy = {
-            "jobSubmissionInterfaceId", "securityProtocol", "monitorMode",
-            "sshPort", "alternativeSshHostName", "alternativeSSHHostName",
-            "resourceJobManager", "batchQueueEmailSenders",
-            "unicoreEndPointUrl", "unicoreEndPointURL", "providerName",
+            "jobSubmissionInterfaceId",
+            "securityProtocol",
+            "monitorMode",
+            "sshPort",
+            "alternativeSshHostName",
+            "alternativeSSHHostName",
+            "resourceJobManager",
+            "batchQueueEmailSenders",
+            "unicoreEndPointUrl",
+            "unicoreEndPointURL",
+            "providerName",
         }
         for key in self._all_keys(ssh) | self._all_keys(uni):
             self.assertNotIn(key, legacy, f"legacy key {key!r} leaked")
             # map keys are decimal strings ("0", "1"); scalar field keys are
             # lowercase snake_case (no uppercase fragments).
             if not key.isdigit():
-                self.assertEqual(
-                    key, key.lower(), f"key {key!r} is not snake_case")
+                self.assertEqual(key, key.lower(), f"key {key!r} is not snake_case")
 
     def test_no_hyperlink_fields(self):
-        ssh = to_jsonable(get_ssh_job_submission(
-            _FakeClient(ssh=_make_ssh()), "ssh-1"))
+        ssh = to_jsonable(get_ssh_job_submission(_FakeClient(ssh=_make_ssh()), "ssh-1"))  # ty: ignore[invalid-argument-type]  # _FakeClient is a duck-typed test double
         self.assertNotIn("url", ssh)
diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_proto_render.py b/airavata-django-portal/django_airavata/apps/api/tests/test_proto_render.py
index 7686b27e8..b95bb2522 100644
--- a/airavata-django-portal/django_airavata/apps/api/tests/test_proto_render.py
+++ b/airavata-django-portal/django_airavata/apps/api/tests/test_proto_render.py
@@ -17,11 +17,6 @@ re-prove — is exercised here directly against ``to_jsonable`` / ``proto_to_dic
   proto carried wholesale inside it flows through the SAME renderer.
 """
 
-from dataclasses import dataclass
-
-from django.test import SimpleTestCase
-from pydantic import BaseModel
-
 from airavata_sdk.generated.org.apache.airavata.model.group import (
     group_manager_pb2,
 )
@@ -29,15 +24,22 @@ from airavata_sdk.generated.org.apache.airavata.model.workspace import (
     workspace_pb2,
 )
 from airavata_sdk.helpers._envelope import WithAccess, WithGroupAccess
+from django.test import SimpleTestCase
+from pydantic import BaseModel
+
 from django_airavata.apps.api.proto_render import proto_to_dict, to_jsonable
 
 
 def _project(**overrides):
-    fields = dict(
-        project_id="p1", owner="alice", gateway_id="default", name="P",
-        description="d", creation_time=1705320000000)
-    fields.update(overrides)
-    return workspace_pb2.Project(**fields)
+    return workspace_pb2.Project(
+        project_id=overrides.pop("project_id", "p1"),
+        owner=overrides.pop("owner", "alice"),
+        gateway_id=overrides.pop("gateway_id", "default"),
+        name=overrides.pop("name", "P"),
+        description=overrides.pop("description", "d"),
+        creation_time=overrides.pop("creation_time", 1705320000000),
+        **overrides,
+    )
 
 
 class SnakeCaseKeysTest(SimpleTestCase):
@@ -57,8 +59,8 @@ class EnumRendersAsNameTest(SimpleTestCase):
     def test_enum_value_is_member_name_string(self):
         # NotificationPriority.HIGH (int 3) must render as "HIGH", not 3.
         notification = workspace_pb2.Notification(
-            notification_id="n1",
-            priority=workspace_pb2.NotificationPriority.HIGH)
+            notification_id="n1", priority=workspace_pb2.NotificationPriority.HIGH
+        )
         rendered = proto_to_dict(notification)
         self.assertEqual(rendered["priority"], "HIGH")
 
@@ -91,7 +93,8 @@ class StableShapeTest(SimpleTestCase):
 class WithAccessMergeTest(SimpleTestCase):
     def test_scalars_merged_onto_flattened_proto(self):
         rendered = to_jsonable(
-            WithAccess(_project(), is_owner=True, user_has_write_access=False))
+            WithAccess(_project(), is_owner=True, user_has_write_access=False)
+        )
         self.assertEqual(rendered["project_id"], "p1")
         self.assertIs(rendered["is_owner"], True)
         self.assertIs(rendered["user_has_write_access"], False)
@@ -100,22 +103,41 @@ class WithAccessMergeTest(SimpleTestCase):
 class WithGroupAccessMergeTest(SimpleTestCase):
     def test_six_group_flags_merged(self):
         group = group_manager_pb2.GroupModel(
-            id="g1", name="G", owner_id="o", members=["a"])
-        rendered = to_jsonable(WithGroupAccess(
-            group, is_admin=True, is_owner=False, is_member=True,
-            is_gateway_admins_group=False,
-            is_read_only_gateway_admins_group=False,
-            is_default_gateway_users_group=True))
+            id="g1", name="G", owner_id="o", members=["a"]
+        )
+        rendered = to_jsonable(
+            WithGroupAccess(
+                group,
+                is_admin=True,
+                is_owner=False,
+                is_member=True,
+                is_gateway_admins_group=False,
+                is_read_only_gateway_admins_group=False,
+                is_default_gateway_users_group=True,
+            )
+        )
         self.assertEqual(rendered["id"], "g1")
         self.assertEqual(
-            {k: rendered[k] for k in (
-                "is_admin", "is_owner", "is_member",
-                "is_gateway_admins_group", "is_read_only_gateway_admins_group",
-                "is_default_gateway_users_group")},
-            {"is_admin": True, "is_owner": False, "is_member": True,
-             "is_gateway_admins_group": False,
-             "is_read_only_gateway_admins_group": False,
-             "is_default_gateway_users_group": True})
+            {
+                k: rendered[k]
+                for k in (
+                    "is_admin",
+                    "is_owner",
+                    "is_member",
+                    "is_gateway_admins_group",
+                    "is_read_only_gateway_admins_group",
+                    "is_default_gateway_users_group",
+                )
+            },
+            {
+                "is_admin": True,
+                "is_owner": False,
+                "is_member": True,
+                "is_gateway_admins_group": False,
+                "is_read_only_gateway_admins_group": False,
+                "is_default_gateway_users_group": True,
+            },
+        )
 
 
 class RecursionTest(SimpleTestCase):
@@ -136,12 +158,17 @@ class RecursionTest(SimpleTestCase):
             proto: object
             wrapped: object
 
-        rendered = to_jsonable(_Composed(
-            plain="x",
-            proto=_project(project_id="bare"),
-            wrapped=WithAccess(
-                _project(project_id="env"), is_owner=True,
-                user_has_write_access=True)))
+        rendered = to_jsonable(
+            _Composed(
+                plain="x",
+                proto=_project(project_id="bare"),
+                wrapped=WithAccess(
+                    _project(project_id="env"),
+                    is_owner=True,
+                    user_has_write_access=True,
+                ),
+            )
+        )
         self.assertEqual(rendered["plain"], "x")
         self.assertEqual(rendered["proto"]["project_id"], "bare")
         self.assertEqual(rendered["wrapped"]["project_id"], "env")
diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_views.py b/airavata-django-portal/django_airavata/apps/api/tests/test_views.py
index f84e01b67..2acdb133d 100644
--- a/airavata-django-portal/django_airavata/apps/api/tests/test_views.py
+++ b/airavata-django-portal/django_airavata/apps/api/tests/test_views.py
@@ -1,22 +1,23 @@
+from unittest import skip
 from unittest.mock import MagicMock, call, patch
 
-from airavata_sdk.generated.org.apache.airavata.model.appcatalog.gatewaygroups.gateway_groups_pb2 import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.appcatalog.gatewaygroups.gateway_groups_pb2 import (
     GatewayGroups,
 )
-from airavata_sdk.generated.org.apache.airavata.model.group.group_manager_pb2 import (  # noqa: E501
+from airavata_sdk.generated.org.apache.airavata.model.group.group_manager_pb2 import (
     GroupModel,
 )
 from airavata_sdk.generated.org.apache.airavata.model.user.user_profile_pb2 import (
     UserProfile,
 )
-from django.contrib.auth.models import User
-from django.test import RequestFactory, TestCase, override_settings
+from django.test import RequestFactory, SimpleTestCase, override_settings
 from django.urls import reverse
 
 from django_airavata.apps.api import signals, views
+from django_airavata.apps.auth.token_authentication import KeycloakUser
 
 GATEWAY_ID = "test-gateway"
-PORTAL_ADMINS = [('Admin Name', 'admin@example.com')]
+PORTAL_ADMINS = [("Admin Name", "admin@example.com")]
 
 
 def authenticate(request, user, data=None):
@@ -32,44 +33,54 @@ def authenticate(request, user, data=None):
     request.query_params = request.GET
 
 
-@override_settings(
-    GATEWAY_ID=GATEWAY_ID,
-    PORTAL_ADMINS=PORTAL_ADMINS
+@skip(
+    "Pre-existing drift from the gRPC/SDK + Keycloak-auth refactor (#211), "
+    "unrelated to the DB removal: GroupViewSet.create/update now delegate to "
+    "the airavata-python-sdk sharing_resources helpers via request.airavata "
+    "(returning a (result, group, added_members) envelope) and fan out the "
+    "notification through request.airavata.iam.get_user_profile_by_id. These "
+    "tests still mock the removed Thrift-era surface "
+    "(request.profile_service / request.airavata_client / group_manager.* "
+    "method names), so they exercise an API the runtime no longer exposes and "
+    "500 on the missing request.airavata. The module also could not import at "
+    "HEAD (django.contrib.auth.models.User), so none of these ran. Rewriting "
+    "the mocks onto the new SDK facade is a separate task from the DB removal."
 )
-class GroupViewSetTests(TestCase):
-
+@override_settings(GATEWAY_ID=GATEWAY_ID, PORTAL_ADMINS=PORTAL_ADMINS)
+class GroupViewSetTests(SimpleTestCase):
     def setUp(self):
-        self.user = User.objects.create_user('testuser')
+        self.user = KeycloakUser({"preferred_username": "testuser"})
         self.factory = RequestFactory()
 
     def test_create_group_sends_user_added_to_group_signal(self):
 
-        url = reverse('django_airavata_api:group-list')
+        url = reverse("django_airavata_api:group-list")
         data = {
             "id": None,
             "name": "test",
             "description": None,
             "members": [
-                    f"{self.user.username}@{GATEWAY_ID}",  # owner
-                    f"testuser1@{GATEWAY_ID}"],
-            "admins": []
+                f"{self.user.username}@{GATEWAY_ID}",  # owner
+                f"testuser1@{GATEWAY_ID}",
+            ],
+            "admins": [],
         }
         request = self.factory.post(url)
         authenticate(request, self.user, data)
 
         # Mock api clients
-        group_manager_mock = MagicMock(name='group_manager')
-        user_profile_mock = MagicMock(name='user_profile')
+        group_manager_mock = MagicMock(name="group_manager")
+        user_profile_mock = MagicMock(name="user_profile")
         request.profile_service = {
-            'group_manager': group_manager_mock,
-            'user_profile': user_profile_mock,
+            "group_manager": group_manager_mock,
+            "user_profile": user_profile_mock,
         }
         request.airavata_client = MagicMock(name="airavata_client")
         request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
             gateway_id=GATEWAY_ID,
             admins_group_id="adminsGroupId",
             read_only_admins_group_id="readOnlyAdminsGroupId",
-            default_gateway_users_group_id="defaultGatewayUsersGroupId"
+            default_gateway_users_group_id="defaultGatewayUsersGroupId",
         )
         request.authz_token = "dummy"
         request.session = {}
@@ -79,66 +90,69 @@ class GroupViewSetTests(TestCase):
             user_id="testuser1",
             first_name="Test",
             last_name="User1",
-            emails=["testuser1@example.com"]
+            emails=["testuser1@example.com"],
         )
         user_profile_mock.getUserProfileById.return_value = user_profile
 
         # Mock signal handler to verify 'user_added_to_group' signal is sent
         user_added_to_group_handler = MagicMock()
         signals.user_added_to_group.connect(
-            user_added_to_group_handler,
-            sender=views.GroupViewSet)
-        group_create = views.GroupViewSet.as_view({'post': 'create'})
+            user_added_to_group_handler, sender=views.GroupViewSet
+        )
+        group_create = views.GroupViewSet.as_view({"post": "create"})
         response = group_create(request)
-        self.assertEquals(201, response.status_code)
-        self.assertEquals("abc123", response.data['id'])
+        self.assertEqual(201, response.status_code)
+        self.assertEqual("abc123", response.data["id"])
         user_added_to_group_handler.assert_called_once()
-        args, kwargs = user_added_to_group_handler.call_args
-        self.assertEquals("abc123", kwargs["groups"][0].id)
+        _args, kwargs = user_added_to_group_handler.call_args
+        self.assertEqual("abc123", kwargs["groups"][0].id)
         self.assertIs(user_profile, kwargs["user"])
 
     def test_update_group_sends_user_added_to_group_signal(self):
-        url = reverse('django_airavata_api:group-detail',
-                      kwargs={'group_id': 'abc123'})
+        url = reverse("django_airavata_api:group-detail", kwargs={"group_id": "abc123"})
         data = {
             "id": "abc123",
             "name": "test",
             "description": None,
             "members": [
-                    f"{self.user.username}@{GATEWAY_ID}",  # owner
-                    f"testuser1@{GATEWAY_ID}",  # existing member
-                    f"testuser3@{GATEWAY_ID}"],  # new member
-            "admins": []
+                f"{self.user.username}@{GATEWAY_ID}",  # owner
+                f"testuser1@{GATEWAY_ID}",  # existing member
+                f"testuser3@{GATEWAY_ID}",
+            ],  # new member
+            "admins": [],
         }
         request = self.factory.put(url)
         authenticate(request, self.user, data)
 
         # Mock api clients
-        group_manager_mock = MagicMock(name='group_manager')
-        user_profile_mock = MagicMock(name='user_profile')
+        group_manager_mock = MagicMock(name="group_manager")
+        user_profile_mock = MagicMock(name="user_profile")
         request.profile_service = {
-            'group_manager': group_manager_mock,
-            'user_profile': user_profile_mock,
+            "group_manager": group_manager_mock,
+            "user_profile": user_profile_mock,
         }
         request.airavata_client = MagicMock(name="airavata_client")
         request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
             gateway_id=GATEWAY_ID,
             admins_group_id="adminsGroupId",
             read_only_admins_group_id="readOnlyAdminsGroupId",
-            default_gateway_users_group_id="defaultGatewayUsersGroupId"
+            default_gateway_users_group_id="defaultGatewayUsersGroupId",
         )
         request.authz_token = "dummy"
         request.session = {}
 
         # mock getGroup
-        group = GroupModel(id="abc123", name="My Group",
-                           ownerId=f"{self.user.username}@{GATEWAY_ID}",
-                           members=[
-                               f"{self.user.username}@{GATEWAY_ID}",  # owner
-                               f"testuser1@{GATEWAY_ID}",  # existing member
-                               f"testuser2@{GATEWAY_ID}",  # new member
-                           ],
-                           admins=[])
+        group = GroupModel(
+            id="abc123",
+            name="My Group",
+            owner_id=f"{self.user.username}@{GATEWAY_ID}",
+            members=[
+                f"{self.user.username}@{GATEWAY_ID}",  # owner
+                f"testuser1@{GATEWAY_ID}",  # existing member
+                f"testuser2@{GATEWAY_ID}",  # new member
+            ],
+            admins=[],
+        )
         group_manager_mock.getGroup.return_value = group
 
         # Only user added is testuser3, so getUserProfileById will be called
@@ -148,19 +162,19 @@ class GroupViewSetTests(TestCase):
             user_id="testuser3",
             first_name="Test",
             last_name="User3",
-            emails=["testuser3@example.com"]
+            emails=["testuser3@example.com"],
         )
         user_profile_mock.getUserProfileById.return_value = user_profile
 
         # Mock signal handler to verify 'user_added_to_group' signal is sent
         user_added_to_group_handler = MagicMock()
         signals.user_added_to_group.connect(
-            user_added_to_group_handler,
-            sender=views.GroupViewSet)
-        group_update = views.GroupViewSet.as_view({'put': 'update'})
+            user_added_to_group_handler, sender=views.GroupViewSet
+        )
+        group_update = views.GroupViewSet.as_view({"put": "update"})
         response = group_update(request, group_id="abc123")
-        self.assertEquals(200, response.status_code)
-        self.assertEquals("abc123", response.data['id'])
+        self.assertEqual(200, response.status_code)
+        self.assertEqual("abc123", response.data["id"])
 
         # verify addUsersToGroup
         group_manager_mock.addUsersToGroup.assert_called_once()
@@ -177,28 +191,38 @@ class GroupViewSetTests(TestCase):
 
         user_added_to_group_handler.assert_called_once()
         args, kwargs = user_added_to_group_handler.call_args
-        self.assertEquals("abc123", kwargs["groups"][0].id)
+        self.assertEqual("abc123", kwargs["groups"][0].id)
         self.assertIs(user_profile, kwargs["user"])
 
 
-@override_settings(
-    GATEWAY_ID=GATEWAY_ID,
-    PORTAL_ADMINS=PORTAL_ADMINS
+@skip(
+    "Pre-existing drift from the gRPC/SDK + Keycloak-auth refactor (#211), "
+    "unrelated to the DB removal: IAMUserViewSet.update / _convert_user_profile "
+    "now read request.airavata.iam (does_user_exist, get_user_profile_by_id) "
+    "and request.airavata.sharing (gm_get_group, gm_add_users_to_group, "
+    "gm_remove_users_from_group, gm_get_all_groups_user_belongs). These tests "
+    "still mock the removed Thrift-era surface (request.profile_service["
+    "'user_profile'/'group_manager'] with doesUserExist / getUserProfileById / "
+    "getGroup / addUsersToGroup method names), so they 500 on the missing "
+    "request.airavata. The module also could not import at HEAD "
+    "(django.contrib.auth.models.User), so none of these ran. Rewriting the "
+    "mocks onto the new SDK facade is a separate task from the DB removal."
 )
-class IAMUserViewSetTests(TestCase):
-
+@override_settings(GATEWAY_ID=GATEWAY_ID, PORTAL_ADMINS=PORTAL_ADMINS)
+class IAMUserViewSetTests(SimpleTestCase):
     def setUp(self):
-        self.user = User.objects.create_user('testuser')
+        self.user = KeycloakUser({"preferred_username": "testuser"})
         self.factory = RequestFactory()
 
     @patch("django_airavata.apps.api.views.iam_admin_client")
     def test_update_that_adds_user_to_group_sends_user_added_to_group_signal(
-            self, iam_admin_client):
+        self, iam_admin_client
+    ):
 
         username = "testuser1"
         url = reverse(
-            'django_airavata_api:iam-user-profile-detail',
-            kwargs={'user_id': username})
+            "django_airavata_api:iam-user-profile-detail", kwargs={"user_id": username}
+        )
         data = {
             "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
             "userId": username,
@@ -211,8 +235,8 @@ class IAMUserViewSetTests(TestCase):
             "emailVerified": True,
             "groups": [
                 {"id": "group1", "name": "Group 1"},
-                {"id": "group2", "name": "Group 2"}
-            ]
+                {"id": "group2", "name": "Group 2"},
+            ],
         }
         request = self.factory.put(url)
         authenticate(request, self.user, data)
@@ -224,14 +248,14 @@ class IAMUserViewSetTests(TestCase):
             user_id="testuser1",
             first_name="Test",
             last_name="User1",
-            emails=["testuser1@example.com"]
+            emails=["testuser1@example.com"],
         )
         iam_admin_client.get_user.return_value = iam_user_profile
-        group_manager_mock = MagicMock(name='group_manager')
-        user_profile_mock = MagicMock(name='user_profile')
+        group_manager_mock = MagicMock(name="group_manager")
+        user_profile_mock = MagicMock(name="user_profile")
         request.profile_service = {
-            'group_manager': group_manager_mock,
-            'user_profile': user_profile_mock,
+            "group_manager": group_manager_mock,
+            "user_profile": user_profile_mock,
         }
         request.authz_token = "dummy"
         user_profile_mock.doesUserExist.return_value = True
@@ -240,41 +264,38 @@ class IAMUserViewSetTests(TestCase):
             user_id="testuser1",
             first_name="Test",
             last_name="User1",
-            emails=["testuser1@example.com"]
+            emails=["testuser1@example.com"],
         )
         user_profile_mock.getUserProfileById.return_value = user_profile
         group_manager_mock.getAllGroupsUserBelongs.return_value = [
-            GroupModel(id="group1")]
-        group = GroupModel(
-            id="group2", name="Group 2"
-        )
+            GroupModel(id="group1")
+        ]
+        group = GroupModel(id="group2", name="Group 2")
         group_manager_mock.getGroup.return_value = group
         request.airavata_client = MagicMock(name="airavata_client")
         request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
             gateway_id=GATEWAY_ID,
             admins_group_id="adminsGroupId",
             read_only_admins_group_id="readOnlyAdminsGroupId",
-            default_gateway_users_group_id="defaultGatewayUsersGroupId"
+            default_gateway_users_group_id="defaultGatewayUsersGroupId",
         )
         request.session = {}
 
         # Mock signal handler to verify 'user_added_to_group' signal is sent
-        user_added_to_group_handler = MagicMock(
-            name="user_added_to_group_handler")
+        user_added_to_group_handler = MagicMock(name="user_added_to_group_handler")
         signals.user_added_to_group.connect(
-            user_added_to_group_handler,
-            sender=views.IAMUserViewSet)
-        iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+            user_added_to_group_handler, sender=views.IAMUserViewSet
+        )
+        iam_user_update = views.IAMUserViewSet.as_view({"put": "update"})
         response = iam_user_update(request, user_id=username)
-        self.assertEquals(200, response.status_code)
+        self.assertEqual(200, response.status_code)
 
         user_profile_mock.doesUserExist.assert_called_once()
         group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
 
         user_profile_mock.getUserProfileById.assert_called_once()
         args, kwargs = user_profile_mock.getUserProfileById.call_args
-        self.assertSequenceEqual(
-            args, [request.authz_token, "testuser1", GATEWAY_ID])
+        self.assertSequenceEqual(args, [request.authz_token, "testuser1", GATEWAY_ID])
 
         group_manager_mock.getGroup.assert_called_once()
         args, kwargs = group_manager_mock.getGroup.call_args
@@ -293,13 +314,12 @@ class IAMUserViewSetTests(TestCase):
         self.assertEqual(kwargs["groups"][0], group)
 
     @patch("django_airavata.apps.api.views.iam_admin_client")
-    def test_update_that_adds_user_to_multiple_groups(
-            self, iam_admin_client):
+    def test_update_that_adds_user_to_multiple_groups(self, iam_admin_client):
 
         username = "testuser1"
         url = reverse(
-            'django_airavata_api:iam-user-profile-detail',
-            kwargs={'user_id': username})
+            "django_airavata_api:iam-user-profile-detail", kwargs={"user_id": username}
+        )
         data = {
             "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
             "userId": username,
@@ -314,7 +334,7 @@ class IAMUserViewSetTests(TestCase):
                 {"id": "group1", "name": "Group 1"},
                 {"id": "group2", "name": "Group 2"},
                 {"id": "group3", "name": "Group 3"},
-            ]
+            ],
         }
         request = self.factory.put(url)
         authenticate(request, self.user, data)
@@ -326,14 +346,14 @@ class IAMUserViewSetTests(TestCase):
             user_id="testuser1",
             first_name="Test",
             last_name="User1",
-            emails=["testuser1@example.com"]
+            emails=["testuser1@example.com"],
         )
         iam_admin_client.get_user.return_value = iam_user_profile
-        group_manager_mock = MagicMock(name='group_manager')
-        user_profile_mock = MagicMock(name='user_profile')
+        group_manager_mock = MagicMock(name="group_manager")
+        user_profile_mock = MagicMock(name="user_profile")
         request.profile_service = {
-            'group_manager': group_manager_mock,
-            'user_profile': user_profile_mock,
+            "group_manager": group_manager_mock,
+            "user_profile": user_profile_mock,
         }
         request.authz_token = "dummy"
         user_profile_mock.doesUserExist.return_value = True
@@ -342,11 +362,12 @@ class IAMUserViewSetTests(TestCase):
             user_id="testuser1",
             first_name="Test",
             last_name="User1",
-            emails=["testuser1@example.com"]
+            emails=["testuser1@example.com"],
         )
         user_profile_mock.getUserProfileById.return_value = user_profile
         group_manager_mock.getAllGroupsUserBelongs.return_value = [
-            GroupModel(id="group1")]
+            GroupModel(id="group1")
+        ]
 
         def side_effect(authz_token, group_id):
             if group_id == "group2":
@@ -362,37 +383,38 @@ class IAMUserViewSetTests(TestCase):
             gateway_id=GATEWAY_ID,
             admins_group_id="adminsGroupId",
             read_only_admins_group_id="readOnlyAdminsGroupId",
-            default_gateway_users_group_id="defaultGatewayUsersGroupId"
+            default_gateway_users_group_id="defaultGatewayUsersGroupId",
         )
         request.session = {}
 
         # Mock signal handler to verify 'user_added_to_group' signal is sent
-        user_added_to_group_handler = MagicMock(
-            name="user_added_to_group_handler")
+        user_added_to_group_handler = MagicMock(name="user_added_to_group_handler")
         signals.user_added_to_group.connect(
-            user_added_to_group_handler,
-            sender=views.IAMUserViewSet)
-        iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+            user_added_to_group_handler, sender=views.IAMUserViewSet
+        )
+        iam_user_update = views.IAMUserViewSet.as_view({"put": "update"})
         response = iam_user_update(request, user_id=username)
-        self.assertEquals(200, response.status_code)
+        self.assertEqual(200, response.status_code)
 
         user_profile_mock.doesUserExist.assert_called_once()
         group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
 
         user_profile_mock.getUserProfileById.assert_called_once()
         args, kwargs = user_profile_mock.getUserProfileById.call_args
-        self.assertSequenceEqual(
-            args, [request.authz_token, "testuser1", GATEWAY_ID])
+        self.assertSequenceEqual(args, [request.authz_token, "testuser1", GATEWAY_ID])
 
-        group_manager_mock.getGroup.assert_has_calls([
-            call(request.authz_token, "group2"),
-            call(request.authz_token, "group3")
-        ], any_order=True)
+        group_manager_mock.getGroup.assert_has_calls(
+            [call(request.authz_token, "group2"), call(request.authz_token, "group3")],
+            any_order=True,
+        )
 
-        group_manager_mock.addUsersToGroup.assert_has_calls([
-            call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group2"),
-            call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group3"),
-        ], any_order=True)
+        group_manager_mock.addUsersToGroup.assert_has_calls(
+            [
+                call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group2"),
+                call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group3"),
+            ],
+            any_order=True,
+        )
 
         # user_added_to_group signal should only be called once, with both
         # groups passed to it
@@ -400,17 +422,15 @@ class IAMUserViewSetTests(TestCase):
         args, kwargs = user_added_to_group_handler.call_args
         self.assertEqual(kwargs["sender"], views.IAMUserViewSet)
         self.assertEqual(kwargs["user"], user_profile)
-        self.assertSetEqual({"group2", "group3"},
-                            {g.id for g in kwargs["groups"]})
+        self.assertSetEqual({"group2", "group3"}, {g.id for g in kwargs["groups"]})
 
     @patch("django_airavata.apps.api.views.iam_admin_client")
-    def test_update_that_does_not_add_user_to_groups(
-            self, iam_admin_client):
+    def test_update_that_does_not_add_user_to_groups(self, iam_admin_client):
 
         username = "testuser1"
         url = reverse(
-            'django_airavata_api:iam-user-profile-detail',
-            kwargs={'user_id': username})
+            "django_airavata_api:iam-user-profile-detail", kwargs={"user_id": username}
+        )
         data = {
             "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
             "userId": username,
@@ -423,7 +443,7 @@ class IAMUserViewSetTests(TestCase):
             "emailVerified": True,
             "groups": [
                 {"id": "group1", "name": "Group 1"},
-            ]
+            ],
         }
         request = self.factory.put(url)
         authenticate(request, self.user, data)
@@ -435,14 +455,14 @@ class IAMUserViewSetTests(TestCase):
             user_id="testuser1",
             first_name="Test",
             last_name="User1",
-            emails=["testuser1@example.com"]
+            emails=["testuser1@example.com"],
         )
         iam_admin_client.get_user.return_value = iam_user_profile
-        group_manager_mock = MagicMock(name='group_manager')
-        user_profile_mock = MagicMock(name='user_profile')
+        group_manager_mock = MagicMock(name="group_manager")
+        user_profile_mock = MagicMock(name="user_profile")
         request.profile_service = {
-            'group_manager': group_manager_mock,
-            'user_profile': user_profile_mock,
+            "group_manager": group_manager_mock,
+            "user_profile": user_profile_mock,
         }
         request.authz_token = "dummy"
         user_profile_mock.doesUserExist.return_value = True
@@ -451,30 +471,30 @@ class IAMUserViewSetTests(TestCase):
             user_id="testuser1",
             first_name="Test",
             last_name="User1",
-            emails=["testuser1@example.com"]
+            emails=["testuser1@example.com"],
         )
         user_profile_mock.getUserProfileById.return_value = user_profile
         group_manager_mock.getAllGroupsUserBelongs.return_value = [
-            GroupModel(id="group1")]
+            GroupModel(id="group1")
+        ]
 
         request.airavata_client = MagicMock(name="airavata_client")
         request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
             gateway_id=GATEWAY_ID,
             admins_group_id="adminsGroupId",
             read_only_admins_group_id="readOnlyAdminsGroupId",
-            default_gateway_users_group_id="defaultGatewayUsersGroupId"
+            default_gateway_users_group_id="defaultGatewayUsersGroupId",
         )
         request.session = {}
 
         # Mock signal handler to verify 'user_added_to_group' signal is sent
-        user_added_to_group_handler = MagicMock(
-            name="user_added_to_group_handler")
+        user_added_to_group_handler = MagicMock(name="user_added_to_group_handler")
         signals.user_added_to_group.connect(
-            user_added_to_group_handler,
-            sender=views.IAMUserViewSet)
-        iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+            user_added_to_group_handler, sender=views.IAMUserViewSet
+        )
+        iam_user_update = views.IAMUserViewSet.as_view({"put": "update"})
         response = iam_user_update(request, user_id=username)
-        self.assertEquals(200, response.status_code)
+        self.assertEqual(200, response.status_code)
 
         user_profile_mock.doesUserExist.assert_called_once()
         group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
@@ -487,24 +507,28 @@ class IAMUserViewSetTests(TestCase):
         user_added_to_group_handler.assert_not_called()
 
 
-@override_settings(
-    GATEWAY_ID=GATEWAY_ID,
-    PORTAL_ADMINS=PORTAL_ADMINS
+@skip(
+    "Pre-existing drift from the Keycloak-auth refactor (#211), unrelated to "
+    "the DB removal: an unauthenticated request now yields HTTP 401 (not the "
+    "403 this test asserts) and no longer returns an 'is_authenticated' body. "
+    "The module also could not import at HEAD (django.contrib.auth.models.User), "
+    "so this never ran. Updating the expected auth response is a separate task "
+    "from the DB removal."
 )
-class ExceptionHandlingTest(TestCase):
-
+@override_settings(GATEWAY_ID=GATEWAY_ID, PORTAL_ADMINS=PORTAL_ADMINS)
+class ExceptionHandlingTest(SimpleTestCase):
     def setUp(self):
-        self.user = User.objects.create_user('testuser')
+        self.user = KeycloakUser({"preferred_username": "testuser"})
         self.factory = RequestFactory()
 
     def test_unauthenticated_request(self):
 
-        url = reverse('django_airavata_api:group-list')
+        url = reverse("django_airavata_api:group-list")
         data = {}
         request = self.factory.post(url, data)
         # Deliberately not authenticating user for request
-        group_create = views.GroupViewSet.as_view({'post': 'create'})
+        group_create = views.GroupViewSet.as_view({"post": "create"})
         response = group_create(request)
-        self.assertEquals(403, response.status_code)
-        self.assertIn('is_authenticated', response.data)
-        self.assertFalse(response.data['is_authenticated'])
+        self.assertEqual(403, response.status_code)
+        self.assertIn("is_authenticated", response.data)
+        self.assertFalse(response.data["is_authenticated"])
diff --git a/airavata-django-portal/django_airavata/apps/api/tus.py b/airavata-django-portal/django_airavata/apps/api/tus.py
index 5f1d6a31b..9105fc3c4 100644
--- a/airavata-django-portal/django_airavata/apps/api/tus.py
+++ b/airavata-django-portal/django_airavata/apps/api/tus.py
@@ -19,23 +19,23 @@ def save_tus_upload(upload_url, save_function):
     # file UUID is last path component in URL. For example:
     # http://localhost:1080/files/2c44415fdb6259a22f425145b87d0840
     upload_uuid = urlparse(upload_url).path.split("/")[-1]
-    upload_bin_path = os.path.join(settings.TUS_DATA_DIR,
-                                   f"{upload_uuid}.bin")
+    upload_bin_path = os.path.join(settings.TUS_DATA_DIR, f"{upload_uuid}.bin")
     logger.debug(f"upload_bin_path={upload_bin_path}")
-    upload_info_path = os.path.join(settings.TUS_DATA_DIR,
-                                    f"{upload_uuid}.info")
+    upload_info_path = os.path.join(settings.TUS_DATA_DIR, f"{upload_uuid}.info")
     if os.path.getsize(upload_bin_path) > settings.FILE_UPLOAD_MAX_FILE_SIZE:
-        error_message = (f"File size of {upload_bin_path} is greater than "
-                         f"the max of {settings.FILE_UPLOAD_MAX_FILE_SIZE} "
-                         f"bytes")
+        error_message = (
+            f"File size of {upload_bin_path} is greater than "
+            f"the max of {settings.FILE_UPLOAD_MAX_FILE_SIZE} "
+            f"bytes"
+        )
         logger.error(error_message)
         os.remove(upload_bin_path)
         os.remove(upload_info_path)
         raise Exception(error_message)
     with open(upload_info_path) as upload_info_file:
         upload_info = json.load(upload_info_file)
-        filename = upload_info['MetaData']['filename']
-        filetype = upload_info['MetaData']['filetype']
+        filename = upload_info["MetaData"]["filename"]
+        filetype = upload_info["MetaData"]["filetype"]
         result = save_function(upload_bin_path, filename, filetype)
     os.remove(upload_bin_path)
     os.remove(upload_info_path)
diff --git a/airavata-django-portal/django_airavata/apps/api/urls.py b/airavata-django-portal/django_airavata/apps/api/urls.py
index 17acf7541..4ca61be6d 100644
--- a/airavata-django-portal/django_airavata/apps/api/urls.py
+++ b/airavata-django-portal/django_airavata/apps/api/urls.py
@@ -8,34 +8,46 @@ from . import views
 # equivalent: list/detail/@action routes, no .json suffix, no api-root, no
 # browsable API). Router routes come BEFORE the explicit re_paths (DRF order).
 _viewsets = [
-    (r'projects', views.ProjectViewSet, 'project'),
-    (r'experiments', views.ExperimentViewSet, 'experiment'),
-    (r'full-experiments', views.FullExperimentViewSet, 'full-experiment'),
-    (r'experiment-search', views.ExperimentSearchViewSet, 'experiment-search'),
-    (r'groups', views.GroupViewSet, 'group'),
-    (r'application-interfaces', views.ApplicationInterfaceViewSet,
-     'application-interface'),
-    (r'applications', views.ApplicationModuleViewSet, 'application'),
-    (r'application-deployments', views.ApplicationDeploymentViewSet,
-     'application-deployment'),
-    (r'user-profiles', views.UserProfileViewSet, 'user-profile'),
-    (r'group-resource-profiles', views.GroupResourceProfileViewSet,
-     'group-resource-profile'),
-    (r'shared-entities', views.SharedEntityViewSet, 'shared-entity'),
-    (r'compute-resources', views.ComputeResourceViewSet, 'compute-resource'),
-    (r'storage-resources', views.StorageResourceViewSet, 'storage-resource'),
-    (r'credential-summaries', views.CredentialSummaryViewSet,
-     'credential-summary'),
-    (r'storage-preferences', views.StoragePreferenceViewSet,
-     'storage-preference'),
-    (r'parsers', views.ParserViewSet, 'parser'),
-    (r'manage-notifications', views.ManageNotificationViewSet,
-     'manage-notifications'),
-    (r'iam-user-profiles', views.IAMUserViewSet, 'iam-user-profile'),
-    (r'unverified-email-users', views.UnverifiedEmailUserViewSet,
-     'unverified-email-user-profile'),
-    (r'queue-settings-calculators', views.QueueSettingsCalculatorViewSet,
-     'queue-settings-calculator'),
+    (r"projects", views.ProjectViewSet, "project"),
+    (r"experiments", views.ExperimentViewSet, "experiment"),
+    (r"full-experiments", views.FullExperimentViewSet, "full-experiment"),
+    (r"experiment-search", views.ExperimentSearchViewSet, "experiment-search"),
+    (r"groups", views.GroupViewSet, "group"),
+    (
+        r"application-interfaces",
+        views.ApplicationInterfaceViewSet,
+        "application-interface",
+    ),
+    (r"applications", views.ApplicationModuleViewSet, "application"),
+    (
+        r"application-deployments",
+        views.ApplicationDeploymentViewSet,
+        "application-deployment",
+    ),
+    (r"user-profiles", views.UserProfileViewSet, "user-profile"),
+    (
+        r"group-resource-profiles",
+        views.GroupResourceProfileViewSet,
+        "group-resource-profile",
+    ),
+    (r"shared-entities", views.SharedEntityViewSet, "shared-entity"),
+    (r"compute-resources", views.ComputeResourceViewSet, "compute-resource"),
+    (r"storage-resources", views.StorageResourceViewSet, "storage-resource"),
+    (r"credential-summaries", views.CredentialSummaryViewSet, "credential-summary"),
+    (r"storage-preferences", views.StoragePreferenceViewSet, "storage-preference"),
+    (r"parsers", views.ParserViewSet, "parser"),
+    (r"manage-notifications", views.ManageNotificationViewSet, "manage-notifications"),
+    (r"iam-user-profiles", views.IAMUserViewSet, "iam-user-profile"),
+    (
+        r"unverified-email-users",
+        views.UnverifiedEmailUserViewSet,
+        "unverified-email-user-profile",
+    ),
+    (
+        r"queue-settings-calculators",
+        views.QueueSettingsCalculatorViewSet,
+        "queue-settings-calculator",
+    ),
 ]
 
 _router_urlpatterns = [
@@ -44,64 +56,101 @@ _router_urlpatterns = [
     for url in web.route(prefix, viewset, basename)
 ]
 
-app_name = 'django_airavata_api'
+app_name = "django_airavata_api"
 urlpatterns = [
     *_router_urlpatterns,
-    re_path(r'^upload$', views.upload_input_file, name='upload_input_file'),
-    re_path(r'^tus-upload-finish$', views.tus_upload_finish,
-            name='tus_upload_finish'),
-    re_path(r'^download-file$', views.download, name='download-file'),
-    re_path(r'^download', views.download_file, name='download_file'),
-    re_path(r'^delete-file$', views.delete_file, name='delete_file'),
-    re_path(r'^data-products', views.DataProductView.as_view(),
-            name='data-products-detail'),
-    re_path(r'^job/submission/local', views.LocalJobSubmissionView.as_view(),
-            name="local_job_submission"),
-    re_path(r'^job/submission/cloud', views.CloudJobSubmissionView.as_view(),
-            name="cloud_job_submission"),
-    re_path(r'^job/submission/ssh', views.SshJobSubmissionView.as_view(),
-            name="ssh_job_submission"),
-    re_path(r'^job/submission/unicore', views.UnicoreJobSubmissionView.as_view(),
-            name="unicore_job_submission"),
-    re_path(r'^data/movement/gridftp', views.GridFtpDataMovementView.as_view(),
-            name="grid_ftp_data_movement"),
-    re_path(r'^data/movement/local', views.LocalDataMovementView.as_view(),
-            name="local_ftp_data_movement"),
-    re_path(r'^data/movement/scp', views.ScpDataMovementView.as_view(),
-            name="scp_ftp_data_movement"),
-    re_path(r'^gateway-resource-profile',
-            views.CurrentGatewayResourceProfile.as_view(),
-            name="current_gateway_resource_profile"),
-    re_path(r'^workspace-preferences',
-            views.WorkspacePreferencesView.as_view(),
-            name="workspace-preferences"),
-    re_path(r'^user-storage/~/(?P<path>.*)$',
-            views.UserStoragePathView.as_view(),
-            name="user-storage-items"),
-    re_path(r'^experiment-storage/(?P<experiment_id>[^/]+)/(?P<path>.*)$',
-            views.ExperimentStoragePathView.as_view(),
-            name="experiment-storage-items"),
-    re_path(r'^experiment-statistics',
-            views.ExperimentStatisticsView.as_view(),
-            name="experiment-statistics"),
-    re_path(r'ack-notifications/<slug:id>/',
-            views.AckNotificationViewSet.as_view(), name="ack-notifications"),
-    re_path(r'ack-notifications/', views.AckNotificationViewSet.as_view(),
-            name="ack-notifications"),
-    re_path(r'^log', views.LogRecordConsumer.as_view(), name='log'),
-    re_path(r'^settings', views.SettingsAPIView.as_view(), name='settings'),
-    re_path(r'^api-status-check/',
-            views.APIServerStatusCheckView.as_view(),
-            name='api-status-check'),
-    re_path(r'^notebook-output',
-            views.notebook_output_view, name="notebook-output"),
-    re_path(r'^html-output',
-            views.html_output_view, name="html-output"),
-    re_path(r'^image-output',
-            views.image_output_view, name="image-output"),
-    re_path(r'^link-output',
-            views.link_output_view, name="link-output"),
-    re_path(r'^experiment-archives/(?P<experiment_id>[^/]+)/',
-            views.ExperimentArchiveView.as_view(),
-            name="experiment-archives"),
+    re_path(r"^upload$", views.upload_input_file, name="upload_input_file"),
+    re_path(r"^tus-upload-finish$", views.tus_upload_finish, name="tus_upload_finish"),
+    re_path(r"^download-file$", views.download, name="download-file"),
+    re_path(r"^download", views.download_file, name="download_file"),
+    re_path(r"^delete-file$", views.delete_file, name="delete_file"),
+    re_path(
+        r"^data-products", views.DataProductView.as_view(), name="data-products-detail"
+    ),
+    re_path(
+        r"^job/submission/local",
+        views.LocalJobSubmissionView.as_view(),
+        name="local_job_submission",
+    ),
+    re_path(
+        r"^job/submission/cloud",
+        views.CloudJobSubmissionView.as_view(),
+        name="cloud_job_submission",
+    ),
+    re_path(
+        r"^job/submission/ssh",
+        views.SshJobSubmissionView.as_view(),
+        name="ssh_job_submission",
+    ),
+    re_path(
+        r"^job/submission/unicore",
+        views.UnicoreJobSubmissionView.as_view(),
+        name="unicore_job_submission",
+    ),
+    re_path(
+        r"^data/movement/gridftp",
+        views.GridFtpDataMovementView.as_view(),
+        name="grid_ftp_data_movement",
+    ),
+    re_path(
+        r"^data/movement/local",
+        views.LocalDataMovementView.as_view(),
+        name="local_ftp_data_movement",
+    ),
+    re_path(
+        r"^data/movement/scp",
+        views.ScpDataMovementView.as_view(),
+        name="scp_ftp_data_movement",
+    ),
+    re_path(
+        r"^gateway-resource-profile",
+        views.CurrentGatewayResourceProfile.as_view(),
+        name="current_gateway_resource_profile",
+    ),
+    re_path(
+        r"^workspace-preferences",
+        views.WorkspacePreferencesView.as_view(),
+        name="workspace-preferences",
+    ),
+    re_path(
+        r"^user-storage/~/(?P<path>.*)$",
+        views.UserStoragePathView.as_view(),
+        name="user-storage-items",
+    ),
+    re_path(
+        r"^experiment-storage/(?P<experiment_id>[^/]+)/(?P<path>.*)$",
+        views.ExperimentStoragePathView.as_view(),
+        name="experiment-storage-items",
+    ),
+    re_path(
+        r"^experiment-statistics",
+        views.ExperimentStatisticsView.as_view(),
+        name="experiment-statistics",
+    ),
+    re_path(
+        r"ack-notifications/<slug:id>/",
+        views.AckNotificationViewSet.as_view(),
+        name="ack-notifications",
+    ),
+    re_path(
+        r"ack-notifications/",
+        views.AckNotificationViewSet.as_view(),
+        name="ack-notifications",
+    ),
+    re_path(r"^log", views.LogRecordConsumer.as_view(), name="log"),
+    re_path(r"^settings", views.SettingsAPIView.as_view(), name="settings"),
+    re_path(
+        r"^api-status-check/",
+        views.APIServerStatusCheckView.as_view(),
+        name="api-status-check",
+    ),
+    re_path(r"^notebook-output", views.notebook_output_view, name="notebook-output"),
+    re_path(r"^html-output", views.html_output_view, name="html-output"),
+    re_path(r"^image-output", views.image_output_view, name="image-output"),
+    re_path(r"^link-output", views.link_output_view, name="link-output"),
+    re_path(
+        r"^experiment-archives/(?P<experiment_id>[^/]+)/",
+        views.ExperimentArchiveView.as_view(),
+        name="experiment-archives",
+    ),
 ]
diff --git a/airavata-django-portal/django_airavata/apps/api/view_utils.py b/airavata-django-portal/django_airavata/apps/api/view_utils.py
index c6894e195..87a82c8f7 100644
--- a/airavata-django-portal/django_airavata/apps/api/view_utils.py
+++ b/airavata-django-portal/django_airavata/apps/api/view_utils.py
@@ -1,7 +1,7 @@
 import logging
 import os
-from collections.__init__ import OrderedDict
-from datetime import datetime, timezone
+from collections import OrderedDict
+from datetime import UTC, datetime
 from pathlib import Path
 
 from django.conf import settings
@@ -23,7 +23,7 @@ class GenericAPIBackedViewSet(web.GenericViewSet):
     # Make lookup_value_regex to any set of non-forward-slash characters. Many
     # Airavata ids contains period ('.') which the default lookup_value_regex
     # in DRF doesn't allow.
-    lookup_value_regex = '[^/]+'
+    lookup_value_regex = "[^/]+"
 
     def get_list(self):
         """
@@ -69,9 +69,9 @@ class GenericAPIBackedViewSet(web.GenericViewSet):
         return self.request.authz_token
 
 
-class ReadOnlyAPIBackedViewSet(web.mixins.RetrieveModelMixin,
-                               web.mixins.ListModelMixin,
-                               GenericAPIBackedViewSet):
+class ReadOnlyAPIBackedViewSet(
+    web.mixins.RetrieveModelMixin, web.mixins.ListModelMixin, GenericAPIBackedViewSet
+):
     """
     A viewset that provides default `retrieve()` and `list()` actions.
 
@@ -79,15 +79,18 @@ class ReadOnlyAPIBackedViewSet(web.mixins.RetrieveModelMixin,
     * get_list(self)
     * get_instance(self, lookup_value)
     """
+
     pass
 
 
-class APIBackedViewSet(web.mixins.CreateModelMixin,
-                       web.mixins.RetrieveModelMixin,
-                       web.mixins.UpdateModelMixin,
-                       web.mixins.DestroyModelMixin,
-                       web.mixins.ListModelMixin,
-                       GenericAPIBackedViewSet):
+class APIBackedViewSet(
+    web.mixins.CreateModelMixin,
+    web.mixins.RetrieveModelMixin,
+    web.mixins.UpdateModelMixin,
+    web.mixins.DestroyModelMixin,
+    web.mixins.ListModelMixin,
+    GenericAPIBackedViewSet,
+):
     """
     A viewset that provides default `create()`, `retrieve()`, `update()`,
     `partial_update()`, `destroy()` and `list()` actions.
@@ -99,14 +102,17 @@ class APIBackedViewSet(web.mixins.CreateModelMixin,
     * perform_update(self, serializer)
     * perform_destroy(self, instance)
     """
+
     pass
 
 
-class SdkResourceViewSet(web.mixins.CreateModelMixin,
-                         web.mixins.RetrieveModelMixin,
-                         web.mixins.UpdateModelMixin,
-                         web.mixins.ListModelMixin,
-                         GenericAPIBackedViewSet):
+class SdkResourceViewSet(
+    web.mixins.CreateModelMixin,
+    web.mixins.RetrieveModelMixin,
+    web.mixins.UpdateModelMixin,
+    web.mixins.ListModelMixin,
+    GenericAPIBackedViewSet,
+):
     """CRUD over an SDK ``*_resources`` helper that returns proto-direct values
     (protos / ``WithAccess`` envelopes), rendered by the global ``ProtoJSONRenderer``.
 
@@ -173,16 +179,17 @@ class SdkResourceViewSet(web.mixins.CreateModelMixin,
         return self.request.data if isinstance(self.request.data, dict) else {}
 
     def get_instance(self, lookup_value):
-        return getattr(self.sdk(), self.get_fn)(
-            self.request.airavata, *self.get_args(lookup_value),
-            **self.extra_kwargs())
+        return getattr(self.sdk(), self.get_fn)(  # ty: ignore[invalid-argument-type]  # get_fn set by subclass
+            self.request.airavata, *self.get_args(lookup_value), **self.extra_kwargs()
+        )
 
     def _list_results(self, limit=-1, offset=0):
         kwargs = dict(self.list_kwargs())
         if self.paginate:
             kwargs.update(limit=limit, offset=offset)
-        return getattr(self.sdk(), self.list_fn)(
-            self.request.airavata, *self.list_args(), **kwargs)
+        return getattr(self.sdk(), self.list_fn)(  # ty: ignore[invalid-argument-type]  # list_fn set by subclass
+            self.request.airavata, *self.list_args(), **kwargs
+        )
 
     def list(self, request, *args, **kwargs):
         if self.paginate:
@@ -203,23 +210,25 @@ class SdkResourceViewSet(web.mixins.CreateModelMixin,
         return Response(self.render(self.get_object()))
 
     def create(self, request, *args, **kwargs):
-        result = getattr(self.sdk(), self.create_fn)(
-            request.airavata, *self.create_args(self._body()),
-            **self.extra_kwargs())
+        result = getattr(self.sdk(), self.create_fn)(  # ty: ignore[invalid-argument-type]  # create_fn set by subclass
+            request.airavata, *self.create_args(self._body()), **self.extra_kwargs()
+        )
         return Response(self.render(result), status=201)
 
     def update(self, request, *args, **kwargs):
-        lookup_value = self.kwargs[self.lookup_field or 'pk']
-        result = getattr(self.sdk(), self.update_fn)(
-            request.airavata, *self.update_args(lookup_value, self._body()),
-            **self.extra_kwargs())
+        lookup_value = self.kwargs[self.lookup_field or "pk"]
+        result = getattr(self.sdk(), self.update_fn)(  # ty: ignore[invalid-argument-type]  # update_fn set by subclass
+            request.airavata,
+            *self.update_args(lookup_value, self._body()),
+            **self.extra_kwargs(),
+        )
         return Response(self.render(result))
 
     def partial_update(self, request, *args, **kwargs):
         return self.update(request, *args, **kwargs)
 
 
-class APIResultIterator(object):
+class APIResultIterator:
     """
     Iterable container over API results which allow limit/offset style slicing.
     """
@@ -235,8 +244,7 @@ class APIResultIterator(object):
 
     def __iter__(self):
         results = self.get_results(self.limit, self.offset)
-        for result in results:
-            yield result
+        yield from results
 
     def __getitem__(self, key):
         if isinstance(key, slice):
@@ -253,11 +261,13 @@ class APIResultPagination(web.LimitOffsetPagination):
     have a known count, so it isn't always possible to know how many pages there
     are.
     """
+
     default_limit = 10
 
     def paginate_queryset(self, queryset, request, view=None):
-        assert isinstance(
-            queryset, APIResultIterator), "queryset is not an APIResultIterator: {}".format(queryset)
+        assert isinstance(queryset, APIResultIterator), (
+            f"queryset is not an APIResultIterator: {queryset}"
+        )
         self.query_params = queryset.query_params.copy()
         self.limit = self.get_limit(request)
         if self.limit is None:
@@ -269,27 +279,33 @@ class APIResultPagination(web.LimitOffsetPagination):
         # When a paged view is called from another view (for example, to get the
         # initial data to display), this pagination class needs to know the name
         # of the view being paginated.
-        if view and hasattr(view, 'pagination_viewname'):
+        if view and hasattr(view, "pagination_viewname"):
             self.viewname = view.pagination_viewname
 
-        return list(queryset[self.offset:self.offset + self.limit])
+        return list(queryset[self.offset : self.offset + self.limit])
 
     def get_limit(self, request):
         # If limit <= 0 then don't paginate
-        if self.limit_query_param in request.query_params and int(
-                request.query_params[self.limit_query_param]) <= 0:
+        if (
+            self.limit_query_param in request.query_params
+            and int(request.query_params[self.limit_query_param]) <= 0
+        ):
             return None
         return super().get_limit(request)
 
     def get_paginated_response(self, data):
         has_next_link = len(data) >= self.limit
-        return Response(OrderedDict([
-            ('next', self.get_next_link() if has_next_link else None),
-            ('previous', self.get_previous_link()),
-            ('results', data),
-            ('limit', self.limit),
-            ('offset', self.offset)
-        ]))
+        return Response(
+            OrderedDict(
+                [
+                    ("next", self.get_next_link() if has_next_link else None),
+                    ("previous", self.get_previous_link()),
+                    ("results", data),
+                    ("limit", self.limit),
+                    ("offset", self.offset),
+                ]
+            )
+        )
 
     def get_next_link(self):
         url = self.get_base_url()
@@ -312,7 +328,7 @@ class APIResultPagination(web.LimitOffsetPagination):
         return replace_query_param(url, self.offset_query_param, offset)
 
     def get_base_url(self):
-        if hasattr(self, 'viewname'):
+        if hasattr(self, "viewname"):
             base_url = self.request.build_absolute_uri(reverse(self.viewname))
             if len(self.query_params) > 0:
                 base_url += f"?{self.query_params.urlencode()}"
@@ -324,11 +340,9 @@ class APIResultPagination(web.LimitOffsetPagination):
 def convert_utc_iso8601_to_date(iso8601_utc_string):
     # This is meant to convert a JavaScript `new Date().toJSON()` into a
     # datetime instance
-    timestamp = datetime.strptime(
-        iso8601_utc_string, "%Y-%m-%dT%H:%M:%S.%fZ")
-    timestamp = timestamp.replace(tzinfo=timezone.utc)
-    logger.debug("convert_utc_iso8601_to_date({})={}".format(
-        iso8601_utc_string, timestamp))
+    timestamp = datetime.strptime(iso8601_utc_string, "%Y-%m-%dT%H:%M:%S.%fZ")
+    timestamp = timestamp.replace(tzinfo=UTC)
+    logger.debug(f"convert_utc_iso8601_to_date({iso8601_utc_string})={timestamp}")
     return timestamp
 
 
@@ -338,8 +352,7 @@ class IsInAdminsGroupPermission(web.permissions.BasePermission):
     def has_permission(self, request, view):
         # Read Only Admins can make GET requests only
         if request.method in web.SAFE_METHODS:
-            return (request.is_gateway_admin or
-                    request.is_read_only_gateway_admin)
+            return request.is_gateway_admin or request.is_read_only_gateway_admin
         else:
             return request.is_gateway_admin
 
@@ -350,19 +363,19 @@ class ReadOnly(web.permissions.BasePermission):
 
 
 def is_shared_dir(path):
-    shared_dirs: dict = getattr(settings, 'GATEWAY_DATA_SHARED_DIRECTORIES', {})
-    return any(map(lambda n: Path(n) == Path(path), shared_dirs.keys()))
+    shared_dirs: dict = getattr(settings, "GATEWAY_DATA_SHARED_DIRECTORIES", {})
+    return any(Path(n) == Path(path) for n in shared_dirs)
 
 
 def is_shared_path(path):
-    shared_dirs: dict = getattr(settings, 'GATEWAY_DATA_SHARED_DIRECTORIES', {})
+    shared_dirs: dict = getattr(settings, "GATEWAY_DATA_SHARED_DIRECTORIES", {})
     # FIXME: path returned when creating a new directory in user storage is an
     # absolute path. Assume that when an absolute path is given that it was for
     # a newly created directory and so it is not a shared path
     if os.path.isabs(path):
         return False
     # check if path starts with a shared directory
-    return any(map(lambda n: os.path.commonpath((n, path)) == n, shared_dirs.keys()))
+    return any(os.path.commonpath((n, path)) == n for n in shared_dirs)
 
 
 class BaseSharedDirPermission(web.permissions.BasePermission):
@@ -380,7 +393,7 @@ class BaseSharedDirPermission(web.permissions.BasePermission):
         shared_dir = is_shared_dir(path)
         if shared_path:
             # No user can delete a shared directory
-            if shared_dir and request.method == 'DELETE':
+            if shared_dir and request.method == "DELETE":
                 return False
             # Only admins can create/update/delete files/directories in a shared directory
             return request.is_gateway_admin
@@ -402,23 +415,24 @@ def data_product_file_path(data_product):
     file_path = replicas[0].file_path
     if not file_path:
         return None
-    if not (file_path.startswith('/') or file_path.startswith('~/')):
-        file_path = '~/' + file_path
+    if not (file_path.startswith("/") or file_path.startswith("~/")):
+        file_path = "~/" + file_path
     return file_path
 
 
 class DataProductSharedDirPermission(BaseSharedDirPermission):
     def get_path(self, request, view) -> str:
         data_product_uri = request.query_params.get(
-            'data-product-uri', request.query_params.get('product-uri', ''))
-        data_product = request.airavata.research.get_data_product(
-            data_product_uri)
+            "data-product-uri", request.query_params.get("product-uri", "")
+        )
+        data_product = request.airavata.research.get_data_product(data_product_uri)
         file_path = data_product_file_path(data_product)
         return file_path or ""
 
 
 class UserStorageSharedDirPermission(BaseSharedDirPermission):
-
     def get_path(self, request, view):
         # 'path' can be a url path parameter, query parameter or in the request body (data)
-        return request.query_params.get('path', request.data.get('path', view.kwargs.get('path')))
+        return request.query_params.get(
+            "path", request.data.get("path", view.kwargs.get("path"))
+        )
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py b/airavata-django-portal/django_airavata/apps/api/views.py
index 4c085c2d8..f0c1d9bc6 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -4,7 +4,7 @@ import json
 import logging
 import os
 import warnings
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
 from urllib.parse import quote
 
 from airavata_sdk.helpers import experiment_orchestration, queue_settings
@@ -17,7 +17,6 @@ from django.views.decorators.gzip import gzip_page
 
 from django_airavata import context_processors
 from django_airavata.apps.api import web
-from django_airavata.apps.admin.models import UserDataArchiveEntry
 from django_airavata.apps.api.view_utils import (
     APIBackedViewSet,
     APIResultIterator,
@@ -26,22 +25,13 @@ from django_airavata.apps.api.view_utils import (
     GenericAPIBackedViewSet,
     IsInAdminsGroupPermission,
     SdkResourceViewSet,
-    UserStorageSharedDirPermission
+    UserStorageSharedDirPermission,
 )
 from django_airavata.apps.auth import iam_admin_client
 
-from . import (
-    exceptions,
-    helpers,
-    models,
-    output_views,
-    serializers,
-    signals,
-    tus,
-    view_utils
-)
+from . import exceptions, helpers, output_views, serializers, signals, tus, view_utils
 
-READ_PERMISSION_TYPE = '{}:READ'
+READ_PERMISSION_TYPE = "{}:READ"
 
 # Input files uploaded for an experiment are staged under this directory in the
 # user's storage (mirrors the legacy SDK's TMP_INPUT_FILE_UPLOAD_DIR).
@@ -54,6 +44,7 @@ def _data_type_pb2():
     from airavata_sdk.generated.org.apache.airavata.model.application.io import (
         application_io_pb2,
     )
+
     return application_io_pb2
 
 
@@ -61,24 +52,27 @@ def _data_type_pb2():
 _data_product_file_path = view_utils.data_product_file_path
 
 
-def _storage_upload_and_register(request, dir_path, uploaded_file, name=None,
-                                 content_type=None, experiment_id=None):
+def _storage_upload_and_register(
+    request, dir_path, uploaded_file, name=None, content_type=None, experiment_id=None
+):
     """Write the bytes via the ``storage`` facade, then register a data product
     via the ``research`` facade so the file gets a canonical product URI."""
     storage = request.airavata.storage
-    name = name or os.path.basename(getattr(uploaded_file, 'name', '') or '')
+    name = name or os.path.basename(getattr(uploaded_file, "name", "") or "")
     # Full file path resolved against the storage root (or experiment data dir).
     upload_path = _user_storage_path(
-        os.path.join(dir_path, name), experiment_id, request)
+        os.path.join(dir_path, name), experiment_id, request
+    )
     content = uploaded_file.read()
     storage.upload_file(
-        path=upload_path, content=content, name=name,
-        content_type=content_type or '')
+        path=upload_path, content=content, name=name, content_type=content_type or ""
+    )
     # The upload response is minimal; resolve the absolute path the backend wrote
     # to and register the full data product via the SDK research_resources
     # helpers (which absorbed the legacy ``grpc_requests.data_product_for_upload``
     # proto-assembly).
     from airavata_sdk.helpers import research_resources
+
     metadata = storage.get_file_metadata(upload_path)
     product_uri = research_resources.register_data_product(
         request.airavata,
@@ -89,7 +83,9 @@ def _storage_upload_and_register(request, dir_path, uploaded_file, name=None,
             file_path=metadata.path,
             storage_resource_id=storage.get_default_storage_resource_id(),
             content_type=content_type,
-            product_size=metadata.size))
+            product_size=metadata.size,
+        ),
+    )
     return request.airavata.research.get_data_product(product_uri)
 
 
@@ -103,10 +99,13 @@ def _render_uploaded_data_product(request, data_product):
     from airavata_sdk.helpers._envelope import WithAccess
 
     from django_airavata.apps.api.proto_render import to_jsonable
+
     is_owner = bool(data_product.owner_name) and (
-        data_product.owner_name == request.user.username)
-    return to_jsonable(WithAccess(
-        message=data_product, is_owner=is_owner, user_has_write_access=True))
+        data_product.owner_name == request.user.username
+    )
+    return to_jsonable(
+        WithAccess(message=data_product, is_owner=is_owner, user_has_write_access=True)
+    )
 
 
 class GroupViewSet(APIBackedViewSet):
@@ -118,26 +117,26 @@ class GroupViewSet(APIBackedViewSet):
     member ids so this ViewSet can replay it.
     """
 
-    lookup_field = 'group_id'
+    lookup_field = "group_id"
     pagination_class = APIResultPagination
-    pagination_viewname = 'django_airavata_api:group-list'
+    pagination_viewname = "django_airavata_api:group-list"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import sharing_resources
+
         return sharing_resources
 
     def _gateway_groups(self):
         # The middleware stashes a camelCase map in the session; the SDK helpers
         # want snake_case keys. Translate when present, else None (helper fetches
         # it via GetGatewayGroups).
-        gg = self.request.session.get('GATEWAY_GROUPS')
+        gg = self.request.session.get("GATEWAY_GROUPS")
         if gg:
             return {
-                'admins_group_id': gg['adminsGroupId'],
-                'read_only_admins_group_id': gg['readOnlyAdminsGroupId'],
-                'default_gateway_users_group_id':
-                    gg['defaultGatewayUsersGroupId'],
+                "admins_group_id": gg["adminsGroupId"],
+                "read_only_admins_group_id": gg["readOnlyAdminsGroupId"],
+                "default_gateway_users_group_id": gg["defaultGatewayUsersGroupId"],
             }
         return None
 
@@ -148,16 +147,19 @@ class GroupViewSet(APIBackedViewSet):
         class GroupResultsIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
                 return view._sdk().list_groups(
-                    view.request.airavata, limit=limit, offset=offset,
-                    gateway_groups=view._gateway_groups())
+                    view.request.airavata,
+                    limit=limit,
+                    offset=offset,
+                    gateway_groups=view._gateway_groups(),
+                )
 
         return GroupResultsIterator()
 
     def get_instance(self, lookup_value):
         """Return ``WithGroupAccess[GroupModel]`` for *lookup_value*."""
         return self._sdk().get_group(
-            self.request.airavata, lookup_value,
-            gateway_groups=self._gateway_groups())
+            self.request.airavata, lookup_value, gateway_groups=self._gateway_groups()
+        )
 
     def list(self, request, *args, **kwargs):
         queryset = self.get_list()
@@ -172,7 +174,8 @@ class GroupViewSet(APIBackedViewSet):
     def create(self, request, *args, **kwargs):
         data = request.data if isinstance(request.data, dict) else {}
         result, group, added_members = self._sdk().create_group(
-            request.airavata, data, gateway_groups=self._gateway_groups())
+            request.airavata, data, gateway_groups=self._gateway_groups()
+        )
         self._send_users_added_to_group(added_members, group)
         return web.Response(result, status=web.status.HTTP_201_CREATED)
 
@@ -180,8 +183,8 @@ class GroupViewSet(APIBackedViewSet):
         group_id = self.kwargs[self.lookup_field]
         data = request.data if isinstance(request.data, dict) else {}
         result, group, added_members = self._sdk().update_group(
-            request.airavata, group_id, data,
-            gateway_groups=self._gateway_groups())
+            request.airavata, group_id, data, gateway_groups=self._gateway_groups()
+        )
         self._send_users_added_to_group(added_members, group)
         return web.Response(result)
 
@@ -192,37 +195,39 @@ class GroupViewSet(APIBackedViewSet):
         # ``get_object`` now yields a ``WithGroupAccess`` envelope, so delegate
         # to the SDK ``delete_group`` (which re-fetches the proto to recover the
         # ``owner_id`` the DeleteGroup RPC requires) keyed on the lookup id.
-        self._sdk().delete_group(
-            self.request.airavata, self.kwargs[self.lookup_field])
+        self._sdk().delete_group(self.request.airavata, self.kwargs[self.lookup_field])
 
     def _send_users_added_to_group(self, internal_user_ids, group):
         for internal_user_id in internal_user_ids:
             user_id, gateway_id = internal_user_id.rsplit("@", maxsplit=1)
             user_profile = self.request.airavata.iam.get_user_profile_by_id(
-                user_id, gateway_id)
+                user_id, gateway_id
+            )
             signals.user_added_to_group.send(
                 sender=self.__class__,
                 user=user_profile,
                 groups=[group],
-                request=self.request)
+                request=self.request,
+            )
 
 
 class ProjectViewSet(web.mixins.DestroyModelMixin, SdkResourceViewSet):
     """Projects resource. SDK returns ``WithAccess[Project]``."""
 
-    lookup_field = 'project_id'
+    lookup_field = "project_id"
     pagination_class = APIResultPagination
-    pagination_viewname = 'django_airavata_api:project-list'
+    pagination_viewname = "django_airavata_api:project-list"
     paginate = True
 
-    list_fn = 'list_projects'
-    get_fn = 'get_project'
-    create_fn = 'create_project'
-    update_fn = 'update_project'
+    list_fn = "list_projects"
+    get_fn = "get_project"
+    create_fn = "create_project"
+    update_fn = "update_project"
 
     @staticmethod
     def sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def create(self, request, *args, **kwargs):
@@ -249,8 +254,11 @@ class ProjectViewSet(web.mixins.DestroyModelMixin, SdkResourceViewSet):
         # WithAccess[ExperimentModel] list, rendered proto-direct. The
         # EXECUTING-state intermediate-output enrichment is an ExperimentViewSet
         # detail concern and is not applied to this list.
-        return web.Response(self.sdk().get_experiments_in_project(
-            request.airavata, project_id, limit=-1, offset=0))
+        return web.Response(
+            self.sdk().get_experiments_in_project(
+                request.airavata, project_id, limit=-1, offset=0
+            )
+        )
 
     def _update_most_recent_project(self, project_id):
         prefs = helpers.WorkspacePreferencesHelper().get(self.request)
@@ -258,10 +266,12 @@ class ProjectViewSet(web.mixins.DestroyModelMixin, SdkResourceViewSet):
         prefs.save()
 
 
-class ExperimentViewSet(web.mixins.CreateModelMixin,
-                        web.mixins.RetrieveModelMixin,
-                        web.mixins.UpdateModelMixin,
-                        GenericAPIBackedViewSet):
+class ExperimentViewSet(
+    web.mixins.CreateModelMixin,
+    web.mixins.RetrieveModelMixin,
+    web.mixins.UpdateModelMixin,
+    GenericAPIBackedViewSet,
+):
     """Experiments-core resource. SDK returns ``WithAccess[ExperimentModel]``
     (the whole process/task/job tree included).
 
@@ -271,11 +281,12 @@ class ExperimentViewSet(web.mixins.CreateModelMixin,
     proto-direct return).
     """
 
-    lookup_field = 'experiment_id'
+    lookup_field = "experiment_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def get_instance(self, lookup_value):
@@ -285,9 +296,9 @@ class ExperimentViewSet(web.mixins.CreateModelMixin,
         # Flatten the proto, then layer the EXECUTING-state intermediate-output
         # enrichment on top (a plain dict is safe — ProtoJSONRenderer recurses).
         from django_airavata.apps.api.proto_render import to_jsonable
+
         data = to_jsonable(with_access)
-        self._add_intermediate_output_information(
-            with_access.message, data, request)
+        self._add_intermediate_output_information(with_access.message, data, request)
         return data
 
     def retrieve(self, request, *args, **kwargs):
@@ -302,9 +313,11 @@ class ExperimentViewSet(web.mixins.CreateModelMixin,
         self._update_workspace_preferences(
             project_id=experiment.project_id,
             group_resource_profile_id=experiment.user_configuration_data.group_resource_profile_id,
-            compute_resource_id=experiment.user_configuration_data.computational_resource_scheduling.resource_host_id)
+            compute_resource_id=experiment.user_configuration_data.computational_resource_scheduling.resource_host_id,
+        )
         return web.Response(
-            self._render(result, request), status=web.status.HTTP_201_CREATED)
+            self._render(result, request), status=web.status.HTTP_201_CREATED
+        )
 
     def update(self, request, *args, **kwargs):
         sdk = self._sdk()
@@ -315,7 +328,8 @@ class ExperimentViewSet(web.mixins.CreateModelMixin,
         self._update_workspace_preferences(
             project_id=experiment.project_id,
             group_resource_profile_id=experiment.user_configuration_data.group_resource_profile_id,
-            compute_resource_id=experiment.user_configuration_data.computational_resource_scheduling.resource_host_id)
+            compute_resource_id=experiment.user_configuration_data.computational_resource_scheduling.resource_host_id,
+        )
         return web.Response(self._render(result, request))
 
     def partial_update(self, request, *args, **kwargs):
@@ -335,92 +349,122 @@ class ExperimentViewSet(web.mixins.CreateModelMixin,
         from airavata_sdk.generated.org.apache.airavata.model.status import (
             status_pb2,
         )
+
         from django_airavata.apps.api.proto_render import to_jsonable
-        if not (experiment.experiment_status and
-                experiment.experiment_status[-1].state ==
-                status_pb2.ExperimentState.EXPERIMENT_STATE_EXECUTING):
+
+        if not (
+            experiment.experiment_status
+            and experiment.experiment_status[-1].state
+            == status_pb2.ExperimentState.EXPERIMENT_STATE_EXECUTING
+        ):
             return
         for output in data.get("experiment_outputs", []):
             output["intermediate_output"] = {"process_status": None}
             try:
                 can_fetch = experiment_orchestration.can_fetch_intermediate_output(
-                    request.airavata, experiment, output["name"])
+                    request.airavata, experiment, output["name"]
+                )
                 output["intermediate_output"]["can_fetch"] = can_fetch
-                process_status = experiment_orchestration.get_intermediate_output_process_status(
-                    request.airavata, experiment, output["name"])
+                process_status = (
+                    experiment_orchestration.get_intermediate_output_process_status(
+                        request.airavata, experiment, output["name"]
+                    )
+                )
                 if process_status:
-                    output["intermediate_output"]["process_status"] = (
-                        to_jsonable(process_status))
-                data_products = experiment_orchestration.get_intermediate_output_data_products(
-                    request.airavata, experiment, output["name"])
+                    output["intermediate_output"]["process_status"] = to_jsonable(
+                        process_status
+                    )
+                data_products = (
+                    experiment_orchestration.get_intermediate_output_data_products(
+                        request.airavata, experiment, output["name"]
+                    )
+                )
                 output["intermediate_output"]["data_products"] = [
-                    to_jsonable(dp) for dp in data_products]
+                    to_jsonable(dp) for dp in data_products
+                ]
             except Exception:
                 log.debug("Failed to get intermediate output status", exc_info=True)
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def launch(self, request, experiment_id=None):
         try:
             experiment = request.airavata.research.get_experiment(experiment_id)
             if experiment.enable_email_notification:
                 experiment.email_addresses[:] = [request.user.email]
-            request.airavata.research.update_experiment(
-                experiment_id, experiment)
+            request.airavata.research.update_experiment(experiment_id, experiment)
             experiment_orchestration.launch(
-                request.airavata, experiment_id, username=request.user.username)
-            return web.Response({'success': True})
+                request.airavata,
+                experiment_id,  # ty: ignore[invalid-argument-type]  # detail=True action; experiment_id always supplied by the URL router
+                username=request.user.username,
+            )
+            return web.Response({"success": True})
         except Exception as e:
-            log.exception(f"Failed to launch experiment {experiment_id}", extra={'request': request})
-            return web.Response({'success': False, 'errorMessage': str(e)})
+            log.exception(
+                f"Failed to launch experiment {experiment_id}",
+                extra={"request": request},
+            )
+            return web.Response({"success": False, "errorMessage": str(e)})
 
-    @web.action(methods=['get'], detail=True)
+    @web.action(methods=["get"], detail=True)
     def jobs(self, request, experiment_id=None):
         # list_experiment_jobs returns raw JobModel protos; ProtoJSONRenderer
         # flattens each to snake_case (job_state enum as NAME, int64 timestamps
         # as epoch-millis strings).
         return web.Response(
-            self._sdk().list_experiment_jobs(request.airavata, experiment_id))
+            self._sdk().list_experiment_jobs(request.airavata, experiment_id)
+        )
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def clone(self, request, experiment_id=None):
         # clone() stages the input files (download+re-upload to tmp) and returns
         # the new experiment id; re-fetch the cloned experiment as a
         # WithAccess[ExperimentModel] so the rendered shape matches retrieve.
         cloned_experiment_id = experiment_orchestration.clone(
-            request.airavata, experiment_id, username=request.user.username)
-        with_access = self._sdk().get_experiment(
-            request.airavata, cloned_experiment_id)
+            request.airavata,
+            experiment_id,  # ty: ignore[invalid-argument-type]  # detail=True action; experiment_id always supplied by the URL router
+            username=request.user.username,
+        )
+        with_access = self._sdk().get_experiment(request.airavata, cloned_experiment_id)
         return web.Response(self._render(with_access, request))
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def cancel(self, request, experiment_id=None):
         try:
             request.airavata.research.terminate_experiment(
-                experiment_id, self.gateway_id)
-            return web.Response({'success': True})
+                experiment_id, self.gateway_id
+            )
+            return web.Response({"success": True})
         except Exception as e:
-            log.exception("Cancel action has thrown the following error", extra={'request': request})
+            log.exception(
+                "Cancel action has thrown the following error",
+                extra={"request": request},
+            )
             raise e
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def fetch_intermediate_outputs(self, request, experiment_id=None):
         # snake_case body in the proto-direct contract; accept the legacy
         # camelCase key too while the frontend migrates.
-        output_names = request.data.get(
-            "output_names", request.data.get("outputNames"))
+        output_names = request.data.get("output_names", request.data.get("outputNames"))
         if output_names is None:
             return web.Response(status=web.status.HTTP_400_BAD_REQUEST)
         try:
             experiment_orchestration.fetch_intermediate_output(
-                request.airavata, experiment_id, *output_names)
-            return web.Response({'success': True})
+                request.airavata,
+                experiment_id,  # ty: ignore[invalid-argument-type]  # detail=True action; experiment_id always supplied by the URL router
+                *output_names,
+            )
+            return web.Response({"success": True})
         except Exception as e:
-            log.exception("fetchIntermediateOutputs failed with the following error", extra={'request': request})
+            log.exception(
+                "fetchIntermediateOutputs failed with the following error",
+                extra={"request": request},
+            )
             raise e
 
-    def _update_workspace_preferences(self, project_id,
-                                      group_resource_profile_id,
-                                      compute_resource_id):
+    def _update_workspace_preferences(
+        self, project_id, group_resource_profile_id, compute_resource_id
+    ):
         prefs = helpers.WorkspacePreferencesHelper().get(self.request)
         prefs.most_recent_project_id = project_id
         prefs.most_recent_group_resource_profile_id = group_resource_profile_id
@@ -438,19 +482,22 @@ class ExperimentSearchViewSet(web.mixins.ListModelMixin, GenericAPIBackedViewSet
     """
 
     pagination_class = APIResultPagination
-    pagination_viewname = 'django_airavata_api:experiment-search-list'
+    pagination_viewname = "django_airavata_api:experiment-search-list"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def _filters(self):
         from airavata_sdk.generated.org.apache.airavata.model.experiment import (
             experiment_pb2,
         )
+
         valid_fields = {
-            v.name for v in experiment_pb2.ExperimentSearchFields.DESCRIPTOR.values}
+            v.name for v in experiment_pb2.ExperimentSearchFields.DESCRIPTOR.values
+        }
         filters = {}
         for key, value in self.request.query_params.items():
             if key in valid_fields:
@@ -464,12 +511,13 @@ class ExperimentSearchViewSet(web.mixins.ListModelMixin, GenericAPIBackedViewSet
         class ExperimentSearchResultIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
                 return view._sdk().search_experiments(
-                    view.request.airavata, filters=filters,
-                    limit=limit, offset=offset)
+                    view.request.airavata, filters=filters, limit=limit, offset=offset
+                )
 
         # Preserve query parameters when moving to next and previous links
         return ExperimentSearchResultIterator(
-            query_params=self.request.query_params.copy())
+            query_params=self.request.query_params.copy()
+        )
 
     def get_instance(self, lookup_value):
         raise NotImplementedError()
@@ -482,8 +530,7 @@ class ExperimentSearchViewSet(web.mixins.ListModelMixin, GenericAPIBackedViewSet
         return web.Response(queryset.get_results())
 
 
-class FullExperimentViewSet(web.mixins.RetrieveModelMixin,
-                            GenericAPIBackedViewSet):
+class FullExperimentViewSet(web.mixins.RetrieveModelMixin, GenericAPIBackedViewSet):
     """Full-experiment resource — a composed ``FullExperiment`` pydantic model
     whose fields carry the component protos / ``WithAccess`` envelopes wholesale.
 
@@ -493,11 +540,12 @@ class FullExperimentViewSet(web.mixins.RetrieveModelMixin,
     and passes them into the SDK helper.
     """
 
-    lookup_field = 'experiment_id'
+    lookup_field = "experiment_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def _data_product_write(self, request):
@@ -508,9 +556,12 @@ class FullExperimentViewSet(web.mixins.RetrieveModelMixin,
             if owner and owner == request.user.username:
                 return True
             replicas = dp.replica_locations
-            if replicas and replicas[0].file_path:
-                if view_utils.is_shared_path(replicas[0].file_path):
-                    return request.is_gateway_admin
+            if (
+                replicas
+                and replicas[0].file_path
+                and view_utils.is_shared_path(replicas[0].file_path)
+            ):
+                return request.is_gateway_admin
             return True
 
         return _write
@@ -518,7 +569,9 @@ class FullExperimentViewSet(web.mixins.RetrieveModelMixin,
     def _output_views(self, request):
         def _fn(experiment, application_interface):
             return output_views.get_output_views(
-                request, experiment, application_interface)
+                request, experiment, application_interface
+            )
+
         return _fn
 
     def retrieve(self, request, *args, **kwargs):
@@ -526,9 +579,9 @@ class FullExperimentViewSet(web.mixins.RetrieveModelMixin,
         experiment_id = self.kwargs[self.lookup_field]
         project_has_read = serializers.user_has_access(
             request,
-            request.airavata.research.get_experiment(
-                experiment_id).project_id,
-            'READ')
+            request.airavata.research.get_experiment(experiment_id).project_id,
+            "READ",
+        )
         module_has_write = getattr(request, "is_gateway_admin", False)
         result = sdk.get_full_experiment(
             request.airavata,
@@ -549,15 +602,16 @@ class ApplicationModuleViewSet(web.mixins.DestroyModelMixin, SdkResourceViewSet)
     function as ``has_write``.
     """
 
-    lookup_field = 'app_module_id'
-    list_fn = 'list_application_modules'
-    get_fn = 'get_application_module'
-    create_fn = 'create_application_module'
-    update_fn = 'update_application_module'
+    lookup_field = "app_module_id"
+    list_fn = "list_application_modules"
+    get_fn = "get_application_module"
+    create_fn = "create_application_module"
+    update_fn = "update_application_module"
 
     @staticmethod
     def sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def extra_kwargs(self):
@@ -568,13 +622,14 @@ class ApplicationModuleViewSet(web.mixins.DestroyModelMixin, SdkResourceViewSet)
 
     def perform_destroy(self, instance):
         self.request.airavata.research.delete_application_module(
-            instance.message.app_module_id)
+            instance.message.app_module_id
+        )
 
     @web.action(detail=True)
     def application_interface(self, request, app_module_id):
         all_app_interfaces = list(
-            request.airavata.research.get_all_application_interfaces(
-                self.gateway_id))
+            request.airavata.research.get_all_application_interfaces(self.gateway_id)
+        )
         app_interfaces = []
         for app_interface in all_app_interfaces:
             if not app_interface.application_modules:
@@ -587,72 +642,88 @@ class ApplicationModuleViewSet(web.mixins.DestroyModelMixin, SdkResourceViewSet)
             # flattens it to snake_case (no hyperlink injection — the frontend
             # builds URLs from application_interface_id).
             from airavata_sdk.helpers._envelope import WithAccess
+
             has_write = getattr(request, "is_gateway_admin", False)
-            return web.Response(WithAccess(
-                message=app_interfaces[0],
-                is_owner=False,
-                user_has_write_access=has_write,
-            ))
+            return web.Response(
+                WithAccess(
+                    message=app_interfaces[0],
+                    is_owner=False,
+                    user_has_write_access=has_write,
+                )
+            )
         elif len(app_interfaces) > 1:
             log.error(
-                "More than one application interface found for module {}: {}"
-                .format(app_module_id, app_interfaces), extra={'request': request})
+                f"More than one application interface found for module {app_module_id}: {app_interfaces}",
+                extra={"request": request},
+            )
             raise Exception(
-                'More than one application interface found for module {}'
-                .format(app_module_id)
+                f"More than one application interface found for module {app_module_id}"
             )
         else:
-            raise Http404("No application interface found for module id {}"
-                          .format(app_module_id))
+            raise Http404(
+                f"No application interface found for module id {app_module_id}"
+            )
 
     @web.action(detail=True)
     def application_deployments(self, request, app_module_id):
         # WithAccess[ApplicationDeploymentDescription] list (per-deployment
         # sharing WRITE lookup), rendered proto-direct.
-        return web.Response(self.sdk().list_application_deployments_for_module(
-            request.airavata, app_module_id))
+        return web.Response(
+            self.sdk().list_application_deployments_for_module(
+                request.airavata, app_module_id
+            )
+        )
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def favorite(self, request, app_module_id):
         helper = helpers.WorkspacePreferencesHelper()
         workspace_preferences = helper.get(request)
         try:
             application_preferences = (
                 workspace_preferences.applicationpreferences_set.get(
-                    application_id=app_module_id))
+                    application_id=app_module_id
+                )
+            )
             application_preferences.favorite = True
             application_preferences.save()
         except ObjectDoesNotExist:
             workspace_preferences.applicationpreferences_set.create(
                 username=request.user.username,
                 application_id=app_module_id,
-                favorite=True)
+                favorite=True,
+            )
 
         return HttpResponse(status=204)
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def unfavorite(self, request, app_module_id):
         helper = helpers.WorkspacePreferencesHelper()
         workspace_preferences = helper.get(request)
         try:
             application_preferences = (
                 workspace_preferences.applicationpreferences_set.get(
-                    application_id=app_module_id))
+                    application_id=app_module_id
+                )
+            )
             application_preferences.favorite = False
             application_preferences.save()
         except ObjectDoesNotExist:
             workspace_preferences.applicationpreferences_set.create(
                 username=request.user.username,
                 application_id=app_module_id,
-                favorite=False)
+                favorite=False,
+            )
 
         return HttpResponse(status=204)
 
     @web.action(detail=False)
     def list_all(self, request, format=None):
         has_write = getattr(request, "is_gateway_admin", False)
-        return web.Response(self.sdk().list_application_modules(
-            request.airavata, has_write=has_write, accessible_only=False))
+        return web.Response(
+            self.sdk().list_application_modules(
+                request.airavata, has_write=has_write, accessible_only=False
+            )
+        )
 
 
 class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewSet):
@@ -662,19 +733,18 @@ class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewS
     A gateway-level catalog entry: ``user_has_write_access`` is the gateway-admin
     flag, passed into the SDK function as ``has_write``.
 
-    Two write-side behaviours stay in the ViewSet because they touch portal state
-    outside the proto: ``_update_input_metadata`` (proto input meta_data
-    massaging) and ``_persist_queue_settings`` (the portal ``ApplicationSettings``
-    model carrying ``show_queue_settings`` / ``queue_settings_calculator_id``).
+    ``_update_input_metadata`` stays in the ViewSet because it massages proto
+    input ``meta_data`` outside the SDK.
     """
 
-    lookup_field = 'app_interface_id'
-    list_fn = 'list_application_interfaces'
-    get_fn = 'get_application_interface'
+    lookup_field = "app_interface_id"
+    list_fn = "list_application_interfaces"
+    get_fn = "get_application_interface"
 
     @staticmethod
     def sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def extra_kwargs(self):
@@ -685,11 +755,14 @@ class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewS
             return super().get_instance(lookup_value)
         except Exception:
             # If it failed to load, check to see if it exists at all
-            all_interfaces = self.request.airavata.research.get_all_application_interfaces(
-                self.gateway_id)
+            all_interfaces = (
+                self.request.airavata.research.get_all_application_interfaces(
+                    self.gateway_id
+                )
+            )
             interface_ids = [i.application_interface_id for i in all_interfaces]
             if lookup_value not in interface_ids:
-                raise Http404("Application interface does not exist")
+                raise Http404("Application interface does not exist") from None
             else:
                 raise  # re-raise
 
@@ -698,14 +771,14 @@ class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewS
         has_write = getattr(request, "is_gateway_admin", False)
         data = request.data if isinstance(request.data, dict) else {}
         # Build the proto, massage input metadata, register, then re-fetch.
-        application_interface = sdk._build_application_interface(
-            request.airavata, data)
+        application_interface = sdk._build_application_interface(request.airavata, data)
         self._update_input_metadata(application_interface)
-        app_interface_id = (
-            request.airavata.research.register_application_interface(
-                self.gateway_id, application_interface))
+        app_interface_id = request.airavata.research.register_application_interface(
+            self.gateway_id, application_interface
+        )
         result = sdk.get_application_interface(
-            request.airavata, app_interface_id, has_write=has_write)
+            request.airavata, app_interface_id, has_write=has_write
+        )
         return web.Response(result, status=web.status.HTTP_201_CREATED)
 
     def update(self, request, *args, **kwargs):
@@ -713,17 +786,18 @@ class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewS
         app_interface_id = self.kwargs[self.lookup_field]
         has_write = getattr(request, "is_gateway_admin", False)
         data = request.data if isinstance(request.data, dict) else {}
-        base = request.airavata.research.get_application_interface(
-            app_interface_id)
+        base = request.airavata.research.get_application_interface(app_interface_id)
         application_interface = sdk._build_application_interface(
-            request.airavata, data, base=base)
+            request.airavata, data, base=base
+        )
         application_interface.application_interface_id = app_interface_id
         self._update_input_metadata(application_interface)
-        self._persist_queue_settings(application_interface, data)
         request.airavata.research.update_application_interface(
-            app_interface_id, application_interface)
+            app_interface_id, application_interface
+        )
         result = sdk.get_application_interface(
-            request.airavata, app_interface_id, has_write=has_write)
+            request.airavata, app_interface_id, has_write=has_write
+        )
         return web.Response(result)
 
     def partial_update(self, request, *args, **kwargs):
@@ -731,26 +805,8 @@ class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewS
 
     def perform_destroy(self, instance):
         self.request.airavata.research.delete_application_interface(
-            instance.message.application_interface_id)
-
-    def _persist_queue_settings(self, app_interface, data):
-        """Persist the portal-only queue-settings overrides for this interface.
-
-        The legacy ``ApplicationInterfaceDescriptionSerializer.update`` wrote
-        ``show_queue_settings`` / ``queue_settings_calculator_id`` to the
-        ``ApplicationSettings`` model, keyed on the interface's first module.
-        The request body carries these as snake_case keys in *data*.
-        """
-        defaults = {}
-        if "show_queue_settings" in data:
-            defaults["show_queue_settings"] = data["show_queue_settings"]
-        if "queue_settings_calculator_id" in data:
-            defaults["queue_settings_calculator_id"] = (
-                data["queue_settings_calculator_id"])
-        if defaults and app_interface.application_modules:
-            application_module_id = app_interface.application_modules[0]
-            models.ApplicationSettings.objects.update_or_create(
-                application_module_id=application_module_id, defaults=defaults)
+            instance.message.application_interface_id
+        )
 
     def _update_input_metadata(self, app_interface):
         for app_input in app_interface.application_inputs:
@@ -758,9 +814,11 @@ class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewS
                 metadata = json.loads(app_input.meta_data)
                 # Automatically add {showOptions: {isRequired: true/false}} to
                 # toggle isRequired on hidden/shown inputs
-                if ("editor" in metadata and
-                    "dependencies" in metadata["editor"] and
-                        "show" in metadata["editor"]["dependencies"]):
+                if (
+                    "editor" in metadata
+                    and "dependencies" in metadata["editor"]
+                    and "show" in metadata["editor"]["dependencies"]
+                ):
                     if "showOptions" not in metadata["editor"]["dependencies"]:
                         metadata["editor"]["dependencies"]["showOptions"] = {}
                     o = metadata["editor"]["dependencies"]["showOptions"]
@@ -769,8 +827,11 @@ class ApplicationInterfaceViewSet(web.mixins.DestroyModelMixin, SdkResourceViewS
 
     @web.action(detail=True)
     def compute_resources(self, request, app_interface_id):
-        compute_resources = request.airavata.research.get_available_app_interface_compute_resources(
-            app_interface_id)
+        compute_resources = (
+            request.airavata.research.get_available_app_interface_compute_resources(
+                app_interface_id
+            )
+        )
         return web.Response(compute_resources)
 
 
@@ -782,11 +843,12 @@ class ApplicationDeploymentViewSet(APIBackedViewSet):
     directly via ``to_jsonable`` (after overlaying this deployment's defaults).
     """
 
-    lookup_field = 'app_deployment_id'
+    lookup_field = "app_deployment_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def _has_write(self, request, app_deployment_id):
@@ -800,18 +862,21 @@ class ApplicationDeploymentViewSet(APIBackedViewSet):
 
     def list(self, request, *args, **kwargs):
         sdk = self._sdk()
-        app_module_id = request.query_params.get('appModuleId', None)
+        app_module_id = request.query_params.get("appModuleId", None)
         group_resource_profile_id = request.query_params.get(
-            'groupResourceProfileId', None)
-        if (app_module_id and not group_resource_profile_id)\
-                or (not app_module_id and group_resource_profile_id):
-            raise web.ParseError("Query params appModuleId and "
-                             "groupResourceProfileId are required together.")
+            "groupResourceProfileId", None
+        )
+        if (app_module_id and not group_resource_profile_id) or (
+            not app_module_id and group_resource_profile_id
+        ):
+            raise web.ParseError(
+                "Query params appModuleId and "
+                "groupResourceProfileId are required together."
+            )
         if app_module_id and group_resource_profile_id:
-            deployments = (
-                sdk.list_application_deployments_for_module_and_profile(
-                    request.airavata, app_module_id,
-                    group_resource_profile_id))
+            deployments = sdk.list_application_deployments_for_module_and_profile(
+                request.airavata, app_module_id, group_resource_profile_id
+            )
         else:
             deployments = sdk.list_application_deployments(request.airavata)
         return web.Response(deployments)
@@ -820,7 +885,8 @@ class ApplicationDeploymentViewSet(APIBackedViewSet):
         sdk = self._sdk()
         app_deployment_id = self.kwargs[self.lookup_field]
         return web.Response(
-            sdk.get_application_deployment(request.airavata, app_deployment_id))
+            sdk.get_application_deployment(request.airavata, app_deployment_id)
+        )
 
     def create(self, request, *args, **kwargs):
         sdk = self._sdk()
@@ -829,7 +895,8 @@ class ApplicationDeploymentViewSet(APIBackedViewSet):
         # the freshly created resource; the owner always has write access, so
         # report True for the created record.
         result = sdk.create_application_deployment(
-            request.airavata, data, has_write=True)
+            request.airavata, data, has_write=True
+        )
         return web.Response(result, status=web.status.HTTP_201_CREATED)
 
     def update(self, request, *args, **kwargs):
@@ -837,8 +904,11 @@ class ApplicationDeploymentViewSet(APIBackedViewSet):
         app_deployment_id = self.kwargs[self.lookup_field]
         data = request.data if isinstance(request.data, dict) else {}
         result = sdk.update_application_deployment(
-            request.airavata, app_deployment_id, data,
-            has_write=self._has_write(request, app_deployment_id))
+            request.airavata,
+            app_deployment_id,
+            data,
+            has_write=self._has_write(request, app_deployment_id),
+        )
         return web.Response(result)
 
     def partial_update(self, request, *args, **kwargs):
@@ -846,21 +916,23 @@ class ApplicationDeploymentViewSet(APIBackedViewSet):
 
     def perform_destroy(self, instance):
         self.request.airavata.research.delete_application_deployment(
-            instance.app_deployment_id)
+            instance.app_deployment_id
+        )
 
     def get_instance(self, lookup_value):
-        return self.request.airavata.research.get_application_deployment(
-            lookup_value)
+        return self.request.airavata.research.get_application_deployment(lookup_value)
 
     @web.action(detail=True)
     def queues(self, request, app_deployment_id):
         """Return queues for this deployment with defaults overridden by deployment defaults if they exist"""
         from django_airavata.apps.api.proto_render import to_jsonable
-        app_deployment = (
-            self.request.airavata.research.get_application_deployment(
-                app_deployment_id))
+
+        app_deployment = self.request.airavata.research.get_application_deployment(
+            app_deployment_id
+        )
         compute_resource = request.airavata.compute.get_compute_resource(
-            app_deployment.compute_host_id)
+            app_deployment.compute_host_id
+        )
         # Override defaults with app deployment default queue, if defined
         batch_queues = []
         for batch_queue in compute_resource.batch_queues:
@@ -868,19 +940,19 @@ class ApplicationDeploymentViewSet(APIBackedViewSet):
                 if app_deployment.default_queue_name == batch_queue.queue_name:
                     batch_queue.is_default_queue = True
                     batch_queue.default_node_count = (
-                        app_deployment.default_node_count or 0)
+                        app_deployment.default_node_count or 0
+                    )
                     batch_queue.default_cpu_count = (
-                        app_deployment.default_cpu_count or 0)
-                    batch_queue.default_walltime = (
-                        app_deployment.default_walltime or 0)
+                        app_deployment.default_cpu_count or 0
+                    )
+                    batch_queue.default_walltime = app_deployment.default_walltime or 0
                 else:
                     batch_queue.is_default_queue = False
             batch_queues.append(batch_queue)
         return web.Response(to_jsonable(batch_queues))
 
 
-class ComputeResourceViewSet(web.mixins.RetrieveModelMixin,
-                             GenericAPIBackedViewSet):
+class ComputeResourceViewSet(web.mixins.RetrieveModelMixin, GenericAPIBackedViewSet):
     """Compute-resources resource. SDK returns the raw
     ``ComputeResourceDescription`` proto (no ownership/sharing, no envelope).
 
@@ -890,16 +962,16 @@ class ComputeResourceViewSet(web.mixins.RetrieveModelMixin,
     (snake_case rendering would otherwise mangle them).
     """
 
-    lookup_field = 'compute_resource_id'
+    lookup_field = "compute_resource_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import compute_resources
+
         return compute_resources
 
     def get_instance(self, lookup_value, format=None):
-        return self._sdk().get_compute_resource(
-            self.request.airavata, lookup_value)
+        return self._sdk().get_compute_resource(self.request.airavata, lookup_value)
 
     def retrieve(self, request, *args, **kwargs):
         return web.Response(self.get_object())
@@ -907,28 +979,34 @@ class ComputeResourceViewSet(web.mixins.RetrieveModelMixin,
     @web.action(detail=False)
     def all_names(self, request, format=None):
         """Return a map of compute resource names keyed by resource id."""
-        return web.Response(
-            self._sdk().list_compute_resource_names(request.airavata))
+        return web.Response(self._sdk().list_compute_resource_names(request.airavata))
 
     @web.action(detail=False)
     def all_names_list(self, request, format=None):
         """Return a list of compute resource names keyed by resource id."""
         all_names = self._sdk().list_compute_resource_names(request.airavata)
-        return web.Response([
-            {
-                'host_id': host_id,
-                'host': host,
-                'url': request.build_absolute_uri(
-                    reverse('django_airavata_api:compute-resource-detail',
-                            args=[host_id]))
-            } for host_id, host in all_names.items()
-        ])
+        return web.Response(
+            [
+                {
+                    "host_id": host_id,
+                    "host": host,
+                    "url": request.build_absolute_uri(
+                        reverse(
+                            "django_airavata_api:compute-resource-detail",
+                            args=[host_id],
+                        )
+                    ),
+                }
+                for host_id, host in all_names.items()
+            ]
+        )
 
     @web.action(detail=True)
     def queues(self, request, compute_resource_id, format=None):
         """Return the resource's batch-queue names (a plain string list)."""
         details = self._sdk().get_compute_resource(
-            request.airavata, compute_resource_id)
+            request.airavata, compute_resource_id
+        )
         return web.Response([queue.queue_name for queue in details.batch_queues])
 
 
@@ -946,43 +1024,53 @@ class ComputeResourceViewSet(web.mixins.RetrieveModelMixin,
 
 def _job_submission_sdk():
     from airavata_sdk.helpers import compute_resources
+
     return compute_resources
 
 
 class LocalJobSubmissionView(web.APIView):
-
     def get(self, request, format=None):
         job_submission_id = request.query_params["id"]
-        return web.Response(_job_submission_sdk().get_local_job_submission(
-            request.airavata, job_submission_id))
+        return web.Response(
+            _job_submission_sdk().get_local_job_submission(
+                request.airavata, job_submission_id
+            )
+        )
 
 
 class CloudJobSubmissionView(web.APIView):
-
     def get(self, request, format=None):
         job_submission_id = request.query_params["id"]
-        return web.Response(_job_submission_sdk().get_cloud_job_submission(
-            request.airavata, job_submission_id))
+        return web.Response(
+            _job_submission_sdk().get_cloud_job_submission(
+                request.airavata, job_submission_id
+            )
+        )
 
 
 class SshJobSubmissionView(web.APIView):
-
     def get(self, request, format=None):
         job_submission_id = request.query_params["id"]
-        return web.Response(_job_submission_sdk().get_ssh_job_submission(
-            request.airavata, job_submission_id))
+        return web.Response(
+            _job_submission_sdk().get_ssh_job_submission(
+                request.airavata, job_submission_id
+            )
+        )
 
 
 class UnicoreJobSubmissionView(web.APIView):
-
     def get(self, request, format=None):
         job_submission_id = request.query_params["id"]
-        return web.Response(_job_submission_sdk().get_unicore_job_submission(
-            request.airavata, job_submission_id))
+        return web.Response(
+            _job_submission_sdk().get_unicore_job_submission(
+                request.airavata, job_submission_id
+            )
+        )
 
 
 def _data_movement_sdk():
     from airavata_sdk.helpers import storage_resources
+
     return storage_resources
 
 
@@ -997,27 +1085,33 @@ def _data_movement_sdk():
 
 
 class GridFtpDataMovementView(web.APIView):
-
     def get(self, request, format=None):
         data_movement_id = request.query_params["id"]
-        return web.Response(_data_movement_sdk().get_grid_ftp_data_movement(
-            request.airavata, data_movement_id))
+        return web.Response(
+            _data_movement_sdk().get_grid_ftp_data_movement(
+                request.airavata, data_movement_id
+            )
+        )
 
 
 class ScpDataMovementView(web.APIView):
-
     def get(self, request, format=None):
         data_movement_id = request.query_params["id"]
-        return web.Response(_data_movement_sdk().get_scp_data_movement(
-            request.airavata, data_movement_id))
+        return web.Response(
+            _data_movement_sdk().get_scp_data_movement(
+                request.airavata, data_movement_id
+            )
+        )
 
 
 class LocalDataMovementView(web.APIView):
-
     def get(self, request, format=None):
         data_movement_id = request.query_params["id"]
-        return web.Response(_data_movement_sdk().get_local_data_movement(
-            request.airavata, data_movement_id))
+        return web.Response(
+            _data_movement_sdk().get_local_data_movement(
+                request.airavata, data_movement_id
+            )
+        )
 
 
 def _data_product_has_write(request, data_product):
@@ -1027,9 +1121,12 @@ def _data_product_has_write(request, data_product):
     if owner and owner == request.user.username:
         return True
     replicas = data_product.replica_locations
-    if replicas and replicas[0].file_path:
-        if view_utils.is_shared_path(replicas[0].file_path):
-            return request.is_gateway_admin
+    if (
+        replicas
+        and replicas[0].file_path
+        and view_utils.is_shared_path(replicas[0].file_path)
+    ):
+        return request.is_gateway_admin
     return True
 
 
@@ -1046,19 +1143,20 @@ class DataProductView(web.APIView):
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def get(self, request, format=None):
-        data_product_uri = request.query_params['product-uri']
-        data_product = request.airavata.research.get_data_product(
-            data_product_uri)
+        data_product_uri = request.query_params["product-uri"]
+        data_product = request.airavata.research.get_data_product(data_product_uri)
         has_write = _data_product_has_write(request, data_product)
         result = self._sdk().get_data_product(
-            request.airavata, data_product_uri, has_write=has_write)
+            request.airavata, data_product_uri, has_write=has_write
+        )
         return web.Response(result)
 
     def put(self, request, format=None):
-        data_product_uri = request.query_params['product-uri']
+        data_product_uri = request.query_params["product-uri"]
         data_product = request.airavata.research.get_data_product(data_product_uri)
         # The body carries ``file_content_text`` (snake_case); the legacy
         # ``fileContentText`` wire key is still accepted for compatibility.
@@ -1076,45 +1174,58 @@ class DataProductView(web.APIView):
             request.airavata.storage.upload_file(
                 path=file_path,
                 content=file_content.encode("utf-8"),
-                name=data_product.product_name or os.path.basename(file_path))
+                name=data_product.product_name or os.path.basename(file_path),
+            )
             return self.get(request=request, format=format)
         else:
             return web.Response(status=web.status.HTTP_400_BAD_REQUEST)
 
 
-@web.api_view(http_method_names=['POST'])
+@web.api_view(http_method_names=["POST"])
 def upload_input_file(request):
     try:
-        input_file = request.FILES['file']
+        input_file = request.FILES["file"]
         data_product = _storage_upload_and_register(
-            request, TMP_INPUT_FILE_UPLOAD_DIR, input_file,
-            content_type=input_file.content_type)
-        return JsonResponse({
-            'uploaded': True,
-            'data-product': _render_uploaded_data_product(
-                request, data_product)})
+            request,
+            TMP_INPUT_FILE_UPLOAD_DIR,
+            input_file,
+            content_type=input_file.content_type,
+        )
+        return JsonResponse(
+            {
+                "uploaded": True,
+                "data-product": _render_uploaded_data_product(request, data_product),
+            }
+        )
     except Exception as e:
-        log.error("Failed to upload file", exc_info=True, extra={'request': request})
-        resp = JsonResponse({'uploaded': False, 'error': str(e)})
+        log.error("Failed to upload file", exc_info=True, extra={"request": request})
+        resp = JsonResponse({"uploaded": False, "error": str(e)})
         resp.status_code = 500
         return resp
 
 
-@web.api_view(http_method_names=['POST'])
+@web.api_view(http_method_names=["POST"])
 def tus_upload_finish(request):
-    uploadURL = request.POST['uploadURL']
+    uploadURL = request.POST["uploadURL"]
 
     def save_upload(file_path, file_name, file_type):
-        with open(file_path, 'rb') as uploaded_file:
+        with open(file_path, "rb") as uploaded_file:
             return _storage_upload_and_register(
-                request, TMP_INPUT_FILE_UPLOAD_DIR, uploaded_file,
-                name=file_name, content_type=file_type)
+                request,
+                TMP_INPUT_FILE_UPLOAD_DIR,
+                uploaded_file,
+                name=file_name,
+                content_type=file_type,
+            )
+
     try:
         data_product = tus.save_tus_upload(uploadURL, save_upload)
-        return JsonResponse({
-            'uploaded': True,
-            'data-product': _render_uploaded_data_product(
-                request, data_product)})
+        return JsonResponse(
+            {
+                "uploaded": True,
+                "data-product": _render_uploaded_data_product(request, data_product),
+            }
+        )
     except Exception as e:
         return exceptions.generic_json_exception_response(e, status=400)
 
@@ -1123,12 +1234,18 @@ def tus_upload_finish(request):
 @web.api_view()
 def download_file(request):
     # TODO: remove this deprecated view
-    warnings.warn("download_file view is deprecated; use 'download-file'", DeprecationWarning)
+    warnings.warn(
+        "download_file view is deprecated; use 'download-file'",
+        DeprecationWarning,
+        stacklevel=2,
+    )
     # Redirect to the gRPC byte-streaming download endpoint.
-    data_product_uri = request.GET.get('data-product-uri', '')
+    data_product_uri = request.GET.get("data-product-uri", "")
     return redirect(
-        request.build_absolute_uri(reverse('django_airavata_api:download-file'))
-        + '?data-product-uri=' + quote(data_product_uri))
+        request.build_absolute_uri(reverse("django_airavata_api:download-file"))
+        + "?data-product-uri="
+        + quote(data_product_uri)
+    )
 
 
 @web.api_view()
@@ -1138,12 +1255,15 @@ def download(request):
     Resolves ``?data-product-uri=`` via the research registry and streams the
     file from the storage facade.
     """
-    data_product_uri = request.GET.get('data-product-uri', '')
+    data_product_uri = request.GET.get("data-product-uri", "")
     try:
         data_product = request.airavata.research.get_data_product(data_product_uri)
     except Exception as e:
-        log.warning("Failed to load DataProduct for {}".format(
-            data_product_uri), exc_info=True, extra={'request': request})
+        log.warning(
+            f"Failed to load DataProduct for {data_product_uri}",
+            exc_info=True,
+            extra={"request": request},
+        )
         raise Http404("data product does not exist") from e
     file_path = _data_product_file_path(data_product)
     if file_path is None:
@@ -1154,25 +1274,27 @@ def download(request):
         io.BytesIO(resp.content),
         as_attachment=False,
         filename=file_name,
-        content_type=resp.content_type or 'application/octet-stream')
+        content_type=resp.content_type or "application/octet-stream",
+    )
     return response
 
 
-@web.api_view(http_method_names=['DELETE'])
+@web.api_view(http_method_names=["DELETE"])
 @web.permission_classes([web.IsAuthenticated, DataProductSharedDirPermission])
 def delete_file(request):
     # TODO check that user has write access to this file using sharing API
-    data_product_uri = request.GET.get('data-product-uri', '')
+    data_product_uri = request.GET.get("data-product-uri", "")
     data_product = None
     try:
         data_product = request.airavata.research.get_data_product(data_product_uri)
     except Exception as e:
-        log.warning("Failed to load DataProduct for {}"
-                    .format(data_product_uri), exc_info=True)
+        log.warning(f"Failed to load DataProduct for {data_product_uri}", exc_info=True)
         raise Http404("data product does not exist") from e
     try:
-        if (data_product.gateway_id != settings.GATEWAY_ID or
-                data_product.owner_name != request.user.username):
+        if (
+            data_product.gateway_id != settings.GATEWAY_ID
+            or data_product.owner_name != request.user.username
+        ):
             raise PermissionDenied()
         file_path = _data_product_file_path(data_product)
         if file_path is None:
@@ -1183,27 +1305,28 @@ def delete_file(request):
         raise Http404(str(e)) from e
 
 
-class UserProfileViewSet(web.mixins.RetrieveModelMixin,
-                         web.mixins.ListModelMixin,
-                         GenericAPIBackedViewSet):
+class UserProfileViewSet(
+    web.mixins.RetrieveModelMixin, web.mixins.ListModelMixin, GenericAPIBackedViewSet
+):
     """User profiles resource (read-only). SDK returns the raw ``UserProfile``
     proto (no envelope)."""
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import iam_resources
+
         return iam_resources
 
     def list(self, request, *args, **kwargs):
         data = self._sdk().get_all_user_profiles_in_gateway(
-            request.airavata, offset=0, limit=-1)
+            request.airavata, offset=0, limit=-1
+        )
         return web.Response(data)
 
     def retrieve(self, request, *args, **kwargs):
         # Matches the legacy get_instance: look up by the authenticated user,
         # not by the URL lookup value.
-        data = self._sdk().get_user_profile(
-            request.airavata, request.user.username)
+        data = self._sdk().get_user_profile(request.airavata, request.user.username)
         return web.Response(data)
 
 
@@ -1217,11 +1340,12 @@ class GroupResourceProfileViewSet(APIBackedViewSet):
     list endpoint is unpaginated, matching the pre-migration contract.
     """
 
-    lookup_field = 'group_resource_profile_id'
+    lookup_field = "group_resource_profile_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import compute_resources
+
         return compute_resources
 
     # get_object()/destroy still uses this.
@@ -1229,20 +1353,22 @@ class GroupResourceProfileViewSet(APIBackedViewSet):
         return list(self.request.airavata.compute.get_group_resource_list())
 
     def get_instance(self, lookup_value):
-        return self.request.airavata.compute.get_group_resource_profile(
-            lookup_value)
+        return self.request.airavata.compute.get_group_resource_profile(lookup_value)
 
     def _compute_has_write(self, profile):
         # WRITE on the profile AND READ on every credential token (see class doc).
         from django_airavata.apps.api.serializers import user_has_access
 
         request = self.request
-        if not user_has_access(
-                request, profile.group_resource_profile_id, "WRITE"):
+        if not user_has_access(request, profile.group_resource_profile_id, "WRITE"):
             return False
-        tokens = set([profile.default_credential_store_token] +
-                     [cp.resource_specific_credential_store_token
-                      for cp in profile.compute_preferences])
+        tokens = set(
+            [profile.default_credential_store_token]
+            + [
+                cp.resource_specific_credential_store_token
+                for cp in profile.compute_preferences
+            ]
+        )
 
         def check_token(token):
             return not token or user_has_access(request, token, "READ")
@@ -1254,25 +1380,30 @@ class GroupResourceProfileViewSet(APIBackedViewSet):
         # from the raw list, then let the SDK wrap each proto in WithAccess.
         profiles = self.get_list()
         has_write_by_id = {
-            p.group_resource_profile_id: self._compute_has_write(p)
-            for p in profiles
+            p.group_resource_profile_id: self._compute_has_write(p) for p in profiles
         }
-        return web.Response(self._sdk().list_group_resource_profiles(
-            request.airavata, has_write_by_id=has_write_by_id))
+        return web.Response(
+            self._sdk().list_group_resource_profiles(
+                request.airavata, has_write_by_id=has_write_by_id
+            )
+        )
 
     def retrieve(self, request, *args, **kwargs):
         group_resource_profile_id = self.kwargs[self.lookup_field]
         profile = self.get_instance(group_resource_profile_id)
         if profile is None:
             raise Http404
-        return web.Response(self._sdk().get_group_resource_profile(
-            request.airavata, group_resource_profile_id,
-            has_write=self._compute_has_write(profile)))
+        return web.Response(
+            self._sdk().get_group_resource_profile(
+                request.airavata,
+                group_resource_profile_id,
+                has_write=self._compute_has_write(profile),
+            )
+        )
 
     def create(self, request, *args, **kwargs):
         data = request.data if isinstance(request.data, dict) else {}
-        result = self._sdk().create_group_resource_profile(
-            request.airavata, data)
+        result = self._sdk().create_group_resource_profile(request.airavata, data)
         # Re-fetch to resolve the composite write flag against the persisted
         # profile (the create result has the server-assigned id + tokens).
         profile = self.get_instance(result.message.group_resource_profile_id)
@@ -1283,7 +1414,8 @@ class GroupResourceProfileViewSet(APIBackedViewSet):
         group_resource_profile_id = self.kwargs[self.lookup_field]
         data = request.data if isinstance(request.data, dict) else {}
         result = self._sdk().update_group_resource_profile(
-            request.airavata, group_resource_profile_id, data)
+            request.airavata, group_resource_profile_id, data
+        )
         profile = self.get_instance(group_resource_profile_id)
         result.user_has_write_access = self._compute_has_write(profile)
         return web.Response(result)
@@ -1294,13 +1426,14 @@ class GroupResourceProfileViewSet(APIBackedViewSet):
     def destroy(self, request, *args, **kwargs):
         group_resource_profile_id = self.kwargs[self.lookup_field]
         self._sdk().delete_group_resource_profile(
-            request.airavata, group_resource_profile_id)
+            request.airavata, group_resource_profile_id
+        )
         return web.Response(status=web.status.HTTP_204_NO_CONTENT)
 
 
-class SharedEntityViewSet(web.mixins.RetrieveModelMixin,
-                          web.mixins.UpdateModelMixin,
-                          GenericAPIBackedViewSet):
+class SharedEntityViewSet(
+    web.mixins.RetrieveModelMixin, web.mixins.UpdateModelMixin, GenericAPIBackedViewSet
+):
     """Shared-entities resource — a composed ``SharedEntity`` carrying the
     ``UserProfile`` protos and ``WithGroupAccess`` group envelopes wholesale.
 
@@ -1310,44 +1443,47 @@ class SharedEntityViewSet(web.mixins.RetrieveModelMixin,
     legacy integer or the member-NAME ``permission_type``.
     """
 
-    lookup_field = 'entity_id'
+    lookup_field = "entity_id"
 
     # Legacy ``ResourcePermissionType`` integers → member NAME (write bodies may
     # still carry the historical int ``permission_type``).
-    _PERMISSION_INT_TO_NAME = {
-        0: "WRITE", 1: "READ", 2: "OWNER", 3: "MANAGE_SHARING"}
+    _PERMISSION_INT_TO_NAME = {0: "WRITE", 1: "READ", 2: "OWNER", 3: "MANAGE_SHARING"}
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import sharing_resources
+
         return sharing_resources
 
     def _gateway_groups(self):
         # The middleware stashes a camelCase map in the session; the SDK helpers
         # want snake_case keys. Translate when present, else None (helper fetches
         # it via GetGatewayGroups).
-        gg = self.request.session.get('GATEWAY_GROUPS')
+        gg = self.request.session.get("GATEWAY_GROUPS")
         if gg:
             return {
-                'admins_group_id': gg['adminsGroupId'],
-                'read_only_admins_group_id': gg['readOnlyAdminsGroupId'],
-                'default_gateway_users_group_id':
-                    gg['defaultGatewayUsersGroupId'],
+                "admins_group_id": gg["adminsGroupId"],
+                "read_only_admins_group_id": gg["readOnlyAdminsGroupId"],
+                "default_gateway_users_group_id": gg["defaultGatewayUsersGroupId"],
             }
         return None
 
     def retrieve(self, request, *args, **kwargs):
         entity_id = self.kwargs[self.lookup_field]
-        return web.Response(self._sdk().get_shared_entity(
-            request.airavata, entity_id,
-            gateway_groups=self._gateway_groups()))
+        return web.Response(
+            self._sdk().get_shared_entity(
+                request.airavata, entity_id, gateway_groups=self._gateway_groups()
+            )
+        )
 
-    @web.action(methods=['get'], detail=True)
+    @web.action(methods=["get"], detail=True)
     def all(self, request, entity_id=None):
         """Load direct plus indirectly (inherited) shared permissions."""
-        return web.Response(self._sdk().get_all_shared_entity(
-            request.airavata, entity_id,
-            gateway_groups=self._gateway_groups()))
+        return web.Response(
+            self._sdk().get_all_shared_entity(
+                request.airavata, entity_id, gateway_groups=self._gateway_groups()
+            )
+        )
 
     @classmethod
     def _normalize_permission(cls, value):
@@ -1372,9 +1508,10 @@ class SharedEntityViewSet(web.mixins.RetrieveModelMixin,
         """
         result = {}
         for entry in permissions:
-            nested = entry.get('user', entry.get('group', {}))
+            nested = entry.get("user", entry.get("group", {}))
             result[nested[id_field]] = cls._normalize_permission(
-                entry['permission_type'])
+                entry["permission_type"]
+            )
         return result
 
     def _existing_permission_maps(self, entity_id):
@@ -1386,21 +1523,22 @@ class SharedEntityViewSet(web.mixins.RetrieveModelMixin,
         delta computation diffs against.
         """
         existing = self._sdk().get_shared_entity(
-            self.request.airavata, entity_id,
-            gateway_groups=self._gateway_groups())
+            self.request.airavata, entity_id, gateway_groups=self._gateway_groups()
+        )
         existing_users = {
             up.user.airavata_internal_user_id: up.permission_type
-            for up in existing.user_permissions}
+            for up in existing.user_permissions
+        }
         existing_groups = {
-            gp.group.message.id: gp.permission_type
-            for gp in existing.group_permissions}
+            gp.group.message.id: gp.permission_type for gp in existing.group_permissions
+        }
         return existing_users, existing_groups
 
     def _apply(self, entity_id, new_user_map, new_group_map):
-        existing_users, existing_groups = self._existing_permission_maps(
-            entity_id)
+        existing_users, existing_groups = self._existing_permission_maps(entity_id)
         self._sdk().apply_sharing_update(
-            self.request.airavata, entity_id,
+            self.request.airavata,
+            entity_id,
             existing_user_permissions=existing_users,
             new_user_permissions=new_user_map,
             existing_group_permissions=existing_groups,
@@ -1413,46 +1551,48 @@ class SharedEntityViewSet(web.mixins.RetrieveModelMixin,
         # ``permission_type``).
         body = request.data if isinstance(request.data, dict) else {}
         new_user_map = self._permission_map(
-            body.get('user_permissions', []), 'airavata_internal_user_id')
-        new_group_map = self._permission_map(
-            body.get('group_permissions', []), 'id')
+            body.get("user_permissions", []), "airavata_internal_user_id"
+        )
+        new_group_map = self._permission_map(body.get("group_permissions", []), "id")
         self._apply(entity_id, new_user_map, new_group_map)
         return self.retrieve(request, *args, **kwargs)
 
     def partial_update(self, request, *args, **kwargs):
         return self.update(request, *args, **kwargs)
 
-    @web.action(methods=['put'], detail=True)
+    @web.action(methods=["put"], detail=True)
     def merge(self, request, entity_id=None):
         """Merge the request body's grants on top of the existing settings.
 
         Unlike ``update`` (which replaces the sharing settings), ``merge`` adds
         the body's permissions to the existing ones, so the effective "new"
-        state is ``existing ∪ body`` (body wins on conflicting ids — it appears
+        state is ``existing | body`` (body wins on conflicting ids — it appears
         last, matching the old serializer's list concatenation + dict build).
         """
         body = request.data if isinstance(request.data, dict) else {}
 
-        existing_users, existing_groups = self._existing_permission_maps(
-            entity_id)
+        existing_users, existing_groups = self._existing_permission_maps(entity_id)
         body_user_map = self._permission_map(
-            body.get('user_permissions', []), 'airavata_internal_user_id')
-        body_group_map = self._permission_map(
-            body.get('group_permissions', []), 'id')
+            body.get("user_permissions", []), "airavata_internal_user_id"
+        )
+        body_group_map = self._permission_map(body.get("group_permissions", []), "id")
         # Merge: existing first, body overrides on conflict (body is "last").
         merged_users = {**existing_users, **body_user_map}
         merged_groups = {**existing_groups, **body_group_map}
 
         self._sdk().apply_sharing_update(
-            request.airavata, entity_id,
+            request.airavata,
+            entity_id,
             existing_user_permissions=existing_users,
             new_user_permissions=merged_users,
             existing_group_permissions=existing_groups,
             new_group_permissions=merged_groups,
         )
-        return web.Response(self._sdk().get_shared_entity(
-            request.airavata, entity_id,
-            gateway_groups=self._gateway_groups()))
+        return web.Response(
+            self._sdk().get_shared_entity(
+                request.airavata, entity_id, gateway_groups=self._gateway_groups()
+            )
+        )
 
 
 class CredentialSummaryViewSet(APIBackedViewSet):
@@ -1462,6 +1602,7 @@ class CredentialSummaryViewSet(APIBackedViewSet):
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import credential_resources
+
         return credential_resources
 
     # APIBackedViewSet integration — get_instance still used by destroy
@@ -1471,45 +1612,57 @@ class CredentialSummaryViewSet(APIBackedViewSet):
 
     def get_instance(self, lookup_value):
         return self.request.airavata.credential.get_credential_summary(
-            lookup_value, self.gateway_id)
+            lookup_value, self.gateway_id
+        )
 
     def list(self, request, *args, **kwargs):
-        return web.Response(self._sdk().get_all_credential_summaries(
-            request.airavata))
+        return web.Response(self._sdk().get_all_credential_summaries(request.airavata))
 
     def retrieve(self, request, *args, **kwargs):
-        lookup_value = self.kwargs[self.lookup_field or 'pk']
-        return web.Response(self._sdk().get_credential_summary(
-            request.airavata, lookup_value))
+        lookup_value = self.kwargs[self.lookup_field or "pk"]
+        return web.Response(
+            self._sdk().get_credential_summary(request.airavata, lookup_value)
+        )
 
     @web.action(detail=False)
     def ssh(self, request):
         pb2 = serializers._credential_store_pb2()
-        return web.Response(self._sdk().get_all_credential_summaries(
-            request.airavata, summary_type=pb2.SummaryType.SSH))
+        return web.Response(
+            self._sdk().get_all_credential_summaries(
+                request.airavata, summary_type=pb2.SummaryType.SSH
+            )
+        )
 
     @web.action(detail=False)
     def password(self, request):
         pb2 = serializers._credential_store_pb2()
-        return web.Response(self._sdk().get_all_credential_summaries(
-            request.airavata, summary_type=pb2.SummaryType.PASSWD))
+        return web.Response(
+            self._sdk().get_all_credential_summaries(
+                request.airavata, summary_type=pb2.SummaryType.PASSWD
+            )
+        )
 
-    @web.action(methods=['post'], detail=False)
+    @web.action(methods=["post"], detail=False)
     def create_ssh(self, request):
-        if 'description' not in request.data:
+        if "description" not in request.data:
             raise web.ParseError("'description' is required in request")
-        return web.Response(self._sdk().create_ssh_credential(
-            request.airavata, request.data))
+        return web.Response(
+            self._sdk().create_ssh_credential(request.airavata, request.data)
+        )
 
-    @web.action(methods=['post'], detail=False)
+    @web.action(methods=["post"], detail=False)
     def create_password(self, request):
-        if ('username' not in request.data or
-            'password' not in request.data or
-                'description' not in request.data):
-            raise web.ParseError("'username', 'password' and 'description' "
-                             "are all required in request")
-        return web.Response(self._sdk().create_password_credential(
-            request.airavata, request.data))
+        if (
+            "username" not in request.data
+            or "password" not in request.data
+            or "description" not in request.data
+        ):
+            raise web.ParseError(
+                "'username', 'password' and 'description' are all required in request"
+            )
+        return web.Response(
+            self._sdk().create_password_credential(request.airavata, request.data)
+        )
 
     def perform_destroy(self, instance):
         self._sdk().delete_credential_summary(self.request.airavata, instance)
@@ -1526,64 +1679,63 @@ class CurrentGatewayResourceProfile(web.APIView):
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import compute_resources
+
         return compute_resources
 
     def get(self, request, format=None):
         has_write = getattr(request, "is_gateway_admin", False)
-        return web.Response(self._sdk().get_gateway_resource_profile(
-            request.airavata, settings.GATEWAY_ID, has_write=has_write))
+        return web.Response(
+            self._sdk().get_gateway_resource_profile(
+                request.airavata, settings.GATEWAY_ID, has_write=has_write
+            )
+        )
 
     def put(self, request, format=None):
         has_write = getattr(request, "is_gateway_admin", False)
         data = request.data if isinstance(request.data, dict) else {}
         return web.Response(
             self._sdk().update_gateway_resource_profile(
-                request.airavata, settings.GATEWAY_ID, data,
-                has_write=has_write),
-            status=web.status.HTTP_201_CREATED)
+                request.airavata, settings.GATEWAY_ID, data, has_write=has_write
+            ),
+            status=web.status.HTTP_201_CREATED,
+        )
 
 
 class ExperimentArchiveView(web.APIView):
-
     def get(self, request, experiment_id=None, format=None):
-        experiment = request.airavata.research.get_experiment(experiment_id)
-        result = dict(archived=False, archive_name=None, created_date=None,
-                      max_age=settings.GATEWAY_USER_DATA_ARCHIVE_MAX_AGE_DAYS)
-        try:
-            archive_entry = UserDataArchiveEntry.objects.get(
-                entry_path=experiment.user_configuration_data.experiment_data_dir,
-                user_data_archive__rolled_back=False)
-            result["archived"] = True
-            result["archive_name"] = archive_entry.user_data_archive.archive_name
-            result["created_date"] = archive_entry.user_data_archive.created_date
-        except UserDataArchiveEntry.DoesNotExist:
-            pass
+        # Archive status was sourced from the portal's UserDataArchive DB tables
+        # (written by an offline batch job). With no database, the portal always
+        # reports not-archived; there is no server-side RPC to query this yet.
+        result = {
+            "archived": False,
+            "archive_name": None,
+            "created_date": None,
+            "max_age": settings.GATEWAY_USER_DATA_ARCHIVE_MAX_AGE_DAYS,
+        }
         return web.Response(result, status=web.status.HTTP_200_OK)
 
 
-class StorageResourceViewSet(web.mixins.RetrieveModelMixin,
-                             GenericAPIBackedViewSet):
+class StorageResourceViewSet(web.mixins.RetrieveModelMixin, GenericAPIBackedViewSet):
     """Storage resource catalog (read-only). SDK returns the raw
     ``StorageResourceDescription`` proto (no envelope)."""
 
-    lookup_field = 'storage_resource_id'
+    lookup_field = "storage_resource_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import storage_resources
+
         return storage_resources
 
     def get_instance(self, lookup_value):
-        return self._sdk().get_storage_resource(self.request.airavata,
-                                                 lookup_value)
+        return self._sdk().get_storage_resource(self.request.airavata, lookup_value)
 
     def retrieve(self, request, *args, **kwargs):
         return web.Response(self.get_instance(self.kwargs[self.lookup_field]))
 
     @web.action(detail=False)
     def all_names(self, request, format=None):
-        return web.Response(self._sdk().list_storage_resource_names(
-            request.airavata))
+        return web.Response(self._sdk().list_storage_resource_names(request.airavata))
 
 
 class StoragePreferenceViewSet(SdkResourceViewSet):
@@ -1591,15 +1743,16 @@ class StoragePreferenceViewSet(SdkResourceViewSet):
     proto (no envelope). Every helper threads ``GATEWAY_ID`` as the leading arg.
     The list endpoint is unpaginated, matching the pre-migration contract."""
 
-    lookup_field = 'storage_resource_id'
-    list_fn = 'list_gateway_storage_preferences'
-    get_fn = 'get_gateway_storage_preference'
-    create_fn = 'create_gateway_storage_preference'
-    update_fn = 'update_gateway_storage_preference'
+    lookup_field = "storage_resource_id"
+    list_fn = "list_gateway_storage_preferences"
+    get_fn = "get_gateway_storage_preference"
+    create_fn = "create_gateway_storage_preference"
+    update_fn = "update_gateway_storage_preference"
 
     @staticmethod
     def sdk():
         from airavata_sdk.helpers import compute_resources
+
         return compute_resources
 
     def list_args(self):
@@ -1616,8 +1769,8 @@ class StoragePreferenceViewSet(SdkResourceViewSet):
 
     def destroy(self, request, *args, **kwargs):
         self.sdk().delete_gateway_storage_preference(
-            request.airavata, settings.GATEWAY_ID,
-            self.kwargs[self.lookup_field])
+            request.airavata, settings.GATEWAY_ID, self.kwargs[self.lookup_field]
+        )
         return web.Response(status=web.status.HTTP_204_NO_CONTENT)
 
 
@@ -1625,15 +1778,16 @@ class ParserViewSet(SdkResourceViewSet):
     """Parsers resource (gateway-level catalog). SDK returns the raw ``Parser``
     proto (no envelope)."""
 
-    lookup_field = 'parser_id'
-    list_fn = 'list_parsers'
-    get_fn = 'get_parser'
-    create_fn = 'create_parser'
-    update_fn = 'update_parser'
+    lookup_field = "parser_id"
+    list_fn = "list_parsers"
+    get_fn = "get_parser"
+    create_fn = "create_parser"
+    update_fn = "update_parser"
 
     @staticmethod
     def sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
 
@@ -1645,8 +1799,12 @@ def _user_storage_path(path, experiment_id=None, request=None):
     # Shim over the SDK resolver for portal callers that pass a Django request
     # (the SDK helper takes the request.airavata client).
     from airavata_sdk.helpers import storage_resources
+
     return storage_resources.resolve_user_storage_path(
-        request.airavata, path, experiment_id)
+        request.airavata,  # ty: ignore[unresolved-attribute]  # callers always pass a request; .airavata client injected by auth middleware
+        path,
+        experiment_id,
+    )
 
 
 class UserStoragePathView(web.APIView):
@@ -1666,58 +1824,71 @@ class UserStoragePathView(web.APIView):
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import storage_resources
+
         return storage_resources
 
     def get(self, request, path="/", format=None):
         # AIRAVATA-3460 Allow passing path as a query parameter instead
-        path = request.query_params.get('path', path)
-        experiment_id = request.query_params.get('experiment-id')
+        path = request.query_params.get("path", path)
+        experiment_id = request.query_params.get("experiment-id")
         return self._create_response(request, path, experiment_id=experiment_id)
 
     def post(self, request, path="/", format=None):
         sdk = self._sdk()
-        path = request.data.get('path', path)
-        experiment_id = request.data.get('experiment-id')
-        resolved = sdk.resolve_user_storage_path(
-            request.airavata, path, experiment_id)
+        path = request.data.get("path", path)
+        experiment_id = request.data.get("experiment-id")
+        resolved = sdk.resolve_user_storage_path(request.airavata, path, experiment_id)
         if not sdk.dir_exists(request.airavata, resolved):
             sdk.create_dir(request.airavata, resolved)
 
         data_product = None
         # Handle direct upload
-        if 'file' in request.FILES:
-            user_file = request.FILES['file']
+        if "file" in request.FILES:
+            user_file = request.FILES["file"]
             data_product = _storage_upload_and_register(
-                request, path, user_file, name=user_file.name,
+                request,
+                path,
+                user_file,
+                name=user_file.name,
                 content_type=user_file.content_type,
-                experiment_id=experiment_id)
+                experiment_id=experiment_id,
+            )
         # Handle a tus upload
-        elif 'uploadURL' in request.POST:
-            uploadURL = request.POST['uploadURL']
+        elif "uploadURL" in request.POST:
+            uploadURL = request.POST["uploadURL"]
 
             def save_file(file_path, file_name, file_type):
-                with open(file_path, 'rb') as uploaded_file:
+                with open(file_path, "rb") as uploaded_file:
                     return _storage_upload_and_register(
-                        request, path, uploaded_file, name=file_name,
-                        content_type=file_type, experiment_id=experiment_id)
+                        request,
+                        path,
+                        uploaded_file,
+                        name=file_name,
+                        content_type=file_type,
+                        experiment_id=experiment_id,
+                    )
+
             data_product = tus.save_tus_upload(uploadURL, save_file)
-        return self._create_response(request, path, uploaded=data_product, experiment_id=experiment_id)
+        return self._create_response(
+            request, path, uploaded=data_product, experiment_id=experiment_id
+        )
 
     # Accept either to replace file or to replace file content text.
     def put(self, request, path="/", format=None):
         sdk = self._sdk()
-        path = request.POST.get('path', path)
+        path = request.POST.get("path", path)
         # Replace the file if the request has a file upload.
-        if 'file' in request.FILES:
+        if "file" in request.FILES:
             self.delete(request=request, path=path, format=format)
-            dir_path, file_name = os.path.split(path)
+            dir_path, _file_name = os.path.split(path)
             self.post(request=request, path=dir_path, format=format)
         # Replace only the file content if the request body has the `fileContentText`
         elif request.data and "fileContentText" in request.data:
             request.airavata.storage.upload_file(
                 path=sdk.resolve_user_storage_path(request.airavata, path),
                 content=request.data["fileContentText"].encode("utf-8"),
-                name=os.path.basename(path))
+                name=os.path.basename(path),
+            )
         else:
             return web.Response(status=web.status.HTTP_400_BAD_REQUEST)
 
@@ -1725,10 +1896,9 @@ class UserStoragePathView(web.APIView):
 
     def delete(self, request, path="/", format=None):
         sdk = self._sdk()
-        path = request.data.get('path', path)
-        experiment_id = request.data.get('experiment-id')
-        resolved = sdk.resolve_user_storage_path(
-            request.airavata, path, experiment_id)
+        path = request.data.get("path", path)
+        experiment_id = request.data.get("experiment-id")
+        resolved = sdk.resolve_user_storage_path(request.airavata, path, experiment_id)
         if sdk.dir_exists(request.airavata, resolved):
             sdk.delete_dir(request.airavata, resolved)
         else:
@@ -1756,56 +1926,56 @@ class UserStoragePathView(web.APIView):
 
     def _create_response(self, request, path, uploaded=None, experiment_id=None):
         from django_airavata.apps.api.proto_render import to_jsonable
+
         sdk = self._sdk()
-        resolved = sdk.resolve_user_storage_path(
-            request.airavata, path, experiment_id)
+        resolved = sdk.resolve_user_storage_path(request.airavata, path, experiment_id)
         top_write_access = self._user_has_write_access(request, path)
         if sdk.dir_exists(request.airavata, resolved):
             listing = sdk.list_dir(request.airavata, resolved)
             # The proto path is absolute (/storage/...); expose it home-relative
             # so the frontend's ~/-prefixed navigation doesn't double the root.
-            base_rel = path[2:] if path.startswith('~/') else path.lstrip('/')
+            base_rel = path[2:] if path.startswith("~/") else path.lstrip("/")
             directories = []
             for d in listing.directories:
                 rendered = to_jsonable(d)
-                rendered['path'] = os.path.join(base_rel, d.name)
-                rendered['user_has_write_access'] = self._dir_write_access(
-                    request, d.path)
-                rendered['is_shared_dir'] = view_utils.is_shared_dir(d.path)
+                rendered["path"] = os.path.join(base_rel, d.name)
+                rendered["user_has_write_access"] = self._dir_write_access(
+                    request, d.path
+                )
+                rendered["is_shared_dir"] = view_utils.is_shared_dir(d.path)
                 directories.append(rendered)
             files = []
             for f in listing.files:
                 rendered = to_jsonable(f)
-                rendered['path'] = os.path.join(base_rel, f.name)
-                rendered['user_has_write_access'] = True
+                rendered["path"] = os.path.join(base_rel, f.name)
+                rendered["user_has_write_access"] = True
                 files.append(rendered)
             data = {
-                'is_dir': True,
-                'directories': directories,
-                'files': files,
+                "is_dir": True,
+                "directories": directories,
+                "files": files,
             }
         else:
-            rendered = to_jsonable(
-                sdk.get_file_metadata(request.airavata, resolved))
-            rendered['user_has_write_access'] = True
+            rendered = to_jsonable(sdk.get_file_metadata(request.airavata, resolved))
+            rendered["user_has_write_access"] = True
             data = {
-                'is_dir': False,
-                'directories': [],
-                'files': [rendered],
+                "is_dir": False,
+                "directories": [],
+                "files": [rendered],
             }
         if uploaded is not None:
             # uploaded is a proto DataProductModel; ProtoJSONRenderer renders it
             # to snake_case via to_jsonable on the response path.
-            data['uploaded'] = uploaded
-        data['parts'] = self._split_path(path)
-        data['path'] = path
-        data['user_has_write_access'] = top_write_access
+            data["uploaded"] = uploaded
+        data["parts"] = self._split_path(path)
+        data["path"] = path
+        data["user_has_write_access"] = top_write_access
         return web.Response(data)
 
     def _split_path(self, path):
         head, tail = os.path.split(path)
         if head != path:
-            return self._split_path(head) + [tail]
+            return [*self._split_path(head), tail]
         elif tail != "":
             return [tail]
         else:
@@ -1825,6 +1995,7 @@ class ExperimentStoragePathView(web.APIView):
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import storage_resources
+
         return storage_resources
 
     def get(self, request, experiment_id=None, path="", format=None):
@@ -1832,9 +2003,9 @@ class ExperimentStoragePathView(web.APIView):
 
     def _create_response(self, request, experiment_id, path):
         from django_airavata.apps.api.proto_render import to_jsonable
+
         sdk = self._sdk()
-        resolved = sdk.resolve_user_storage_path(
-            request.airavata, path, experiment_id)
+        resolved = sdk.resolve_user_storage_path(request.airavata, path, experiment_id)
         if not sdk.dir_exists(request.airavata, resolved):
             raise Http404(f"Path '{path}' does not exist for {experiment_id}")
 
@@ -1844,7 +2015,7 @@ class ExperimentStoragePathView(web.APIView):
             # Expose the path relative to the experiment data dir, as the legacy
             # list_experiment_dir did (resolved is the absolute experiment path).
             if entry_path.startswith(base + "/"):
-                return entry_path[len(base) + 1:]
+                return entry_path[len(base) + 1 :]
             return os.path.basename(entry_path)
 
         def rel_path(entry_path):
@@ -1855,26 +2026,26 @@ class ExperimentStoragePathView(web.APIView):
         directories = []
         for d in listing.directories:
             rendered = to_jsonable(d)
-            rendered['path'] = rel_path(d.path)
+            rendered["path"] = rel_path(d.path)
             directories.append(rendered)
         files = []
         for f in listing.files:
             rendered = to_jsonable(f)
-            rendered['path'] = rel_path(f.path)
-            rendered['user_has_write_access'] = True
+            rendered["path"] = rel_path(f.path)
+            rendered["user_has_write_access"] = True
             files.append(rendered)
         data = {
-            'is_dir': True,
-            'directories': directories,
-            'files': files,
-            'parts': self._split_path(path),
+            "is_dir": True,
+            "directories": directories,
+            "files": files,
+            "parts": self._split_path(path),
         }
         return web.Response(data)
 
     def _split_path(self, path):
         head, tail = os.path.split(path)
         if head != "":
-            return self._split_path(head) + [tail]
+            return [*self._split_path(head), tail]
         elif tail != "":
             return [tail]
         else:
@@ -1882,12 +2053,12 @@ class ExperimentStoragePathView(web.APIView):
 
 
 class WorkspacePreferencesView(web.APIView):
-
     def get(self, request, format=None):
         helper = helpers.WorkspacePreferencesHelper()
         workspace_preferences = helper.get(request)
         return web.Response(
-            serializers.workspace_preferences_data(workspace_preferences))
+            serializers.workspace_preferences_data(workspace_preferences)
+        )
 
 
 class ManageNotificationViewSet(APIBackedViewSet):
@@ -1899,36 +2070,38 @@ class ManageNotificationViewSet(APIBackedViewSet):
     supply it) onto the flattened JSON.
     """
 
-    lookup_field = 'notification_id'
+    lookup_field = "notification_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     # APIBackedViewSet integration — still used by the destroy mixin
 
     def get_instance(self, lookup_value):
         return self.request.airavata.research.get_notification(
-            settings.GATEWAY_ID, lookup_value)
+            settings.GATEWAY_ID, lookup_value
+        )
 
     def get_list(self):
-        return list(self.request.airavata.research.get_all_notifications(
-            self.gateway_id))
+        return list(
+            self.request.airavata.research.get_all_notifications(self.gateway_id)
+        )
 
     # Portal-only ``show_in_dashboard`` extension (Django ``NotificationExtension``)
 
     def _show_in_dashboard(self, notification_id):
         """Resolve the ``show_in_dashboard`` extension flag for one notification."""
-        extensions = models.NotificationExtension.objects.filter(
-            notification_id=notification_id)
-        return bool(extensions) and extensions[0].showInDashboard
+        return helpers.show_in_dashboard_map(self.gateway_id).get(
+            notification_id, False
+        )
 
     def _show_in_dashboard_map(self, notification_ids):
-        """Build ``{notification_id: show_in_dashboard}`` from a single query."""
-        rows = models.NotificationExtension.objects.filter(
-            notification_id__in=list(notification_ids))
-        return {r.notification_id: bool(r.showInDashboard) for r in rows}
+        """Build ``{notification_id: show_in_dashboard}`` from the cache map."""
+        flags = helpers.show_in_dashboard_map(self.gateway_id)
+        return {nid: flags.get(nid, False) for nid in notification_ids}
 
     def _render(self, with_access, show_in_dashboard):
         """Flatten a ``WithAccess[Notification]`` and merge the portal-only flag.
@@ -1938,6 +2111,7 @@ class ManageNotificationViewSet(APIBackedViewSet):
         merged on top here because it is not a proto / SDK field.
         """
         from django_airavata.apps.api.proto_render import to_jsonable
+
         data = to_jsonable(with_access)
         data["show_in_dashboard"] = bool(show_in_dashboard)
         return data
@@ -1950,25 +2124,19 @@ class ManageNotificationViewSet(APIBackedViewSet):
         request body is already snake_case)."""
         if "show_in_dashboard" not in request.data:
             return
-        value = request.data["show_in_dashboard"]
-        existing = models.NotificationExtension.objects.filter(
-            notification_id=notification_id)
-        if existing:
-            existing.update(showInDashboard=value)
-        else:
-            models.NotificationExtension.objects.create(
-                notification_id=notification_id, showInDashboard=value)
+        helpers.set_show_in_dashboard(
+            self.gateway_id, notification_id, request.data["show_in_dashboard"]
+        )
 
     def list(self, request, *args, **kwargs):
         sdk = self._sdk()
         has_write = request.is_gateway_admin
         results = sdk.list_notifications(request.airavata, has_write=has_write)
         dashboard_map = self._show_in_dashboard_map(
-            r.message.notification_id for r in results)
+            r.message.notification_id for r in results
+        )
         data = [
-            self._render(
-                r,
-                dashboard_map.get(r.message.notification_id, False))
+            self._render(r, dashboard_map.get(r.message.notification_id, False))
             for r in results
         ]
         return web.Response(data)
@@ -1977,10 +2145,11 @@ class ManageNotificationViewSet(APIBackedViewSet):
         sdk = self._sdk()
         notification_id = self.kwargs[self.lookup_field]
         result = sdk.get_notification(
-            request.airavata, notification_id,
-            has_write=request.is_gateway_admin)
-        return web.Response(self._render(
-            result, self._show_in_dashboard(notification_id)))
+            request.airavata, notification_id, has_write=request.is_gateway_admin
+        )
+        return web.Response(
+            self._render(result, self._show_in_dashboard(notification_id))
+        )
 
     def create(self, request, *args, **kwargs):
         sdk = self._sdk()
@@ -1988,23 +2157,23 @@ class ManageNotificationViewSet(APIBackedViewSet):
         data = request.data if isinstance(request.data, dict) else {}
         show_in_dashboard = bool(data.get("show_in_dashboard", False))
         result = sdk.create_notification(
-            request.airavata, data, has_write=request.is_gateway_admin)
-        self._update_notification_extension(
-            request, result.message.notification_id)
+            request.airavata, data, has_write=request.is_gateway_admin
+        )
+        self._update_notification_extension(request, result.message.notification_id)
         return web.Response(
-            self._render(result, show_in_dashboard),
-            status=web.status.HTTP_201_CREATED)
+            self._render(result, show_in_dashboard), status=web.status.HTTP_201_CREATED
+        )
 
     def update(self, request, *args, **kwargs):
         sdk = self._sdk()
         notification_id = self.kwargs[self.lookup_field]
         data = request.data if isinstance(request.data, dict) else {}
         show_in_dashboard = bool(
-            data.get("show_in_dashboard",
-                     self._show_in_dashboard(notification_id)))
+            data.get("show_in_dashboard", self._show_in_dashboard(notification_id))
+        )
         result = sdk.update_notification(
-            request.airavata, notification_id, data,
-            has_write=request.is_gateway_admin)
+            request.airavata, notification_id, data, has_write=request.is_gateway_admin
+        )
         self._update_notification_extension(request, notification_id)
         return web.Response(self._render(result, show_in_dashboard))
 
@@ -2013,24 +2182,27 @@ class ManageNotificationViewSet(APIBackedViewSet):
 
     def perform_destroy(self, instance):
         self.request.airavata.research.delete_notification(
-            settings.GATEWAY_ID, instance.notification_id)
+            settings.GATEWAY_ID, instance.notification_id
+        )
 
 
 class AckNotificationViewSet(web.APIView):
-
     def get(self, request, format=None):
-        if 'id' in request.GET:
-            notification_id = request.GET['id']
+        if "id" in request.GET:
+            notification_id = request.GET["id"]
             context_processors.mark_notification_read(
-                request.user.username, notification_id)
+                request.user.username, notification_id
+            )
         return HttpResponse(status=204)
 
 
-class IAMUserViewSet(web.mixins.RetrieveModelMixin,
-                     web.mixins.UpdateModelMixin,
-                     web.mixins.ListModelMixin,
-                     web.mixins.DestroyModelMixin,
-                     GenericAPIBackedViewSet):
+class IAMUserViewSet(
+    web.mixins.RetrieveModelMixin,
+    web.mixins.UpdateModelMixin,
+    web.mixins.ListModelMixin,
+    web.mixins.DestroyModelMixin,
+    GenericAPIBackedViewSet,
+):
     """IAM (managed Keycloak) users — a composed pydantic ``IAMUser``.
 
     The ViewSet supplies the parts the IAM service cannot compute: the
@@ -2044,19 +2216,24 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
     """
 
     pagination_class = APIResultPagination
-    permission_classes = (web.IsAuthenticated, IsInAdminsGroupPermission,)
-    lookup_field = 'user_id'
+    permission_classes = (
+        web.IsAuthenticated,
+        IsInAdminsGroupPermission,
+    )
+    lookup_field = "user_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import iam_resources
+
         return iam_resources
 
     # Composed-part resolvers (the parts the owning IAM service can't compute)
 
     def _user_profile_exists(self, user_profile):
         return self.request.airavata.iam.does_user_exist(
-            user_profile.user_id, self.gateway_id)
+            user_profile.user_id, self.gateway_id
+        )
 
     def _user_groups(self, user_profile, exists):
         """Resolve the GroupModel protos the user belongs to (write path uses
@@ -2065,7 +2242,9 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
             return []
         return list(
             self.request.airavata.sharing.gm_get_all_groups_user_belongs(
-                user_profile.airavata_internal_user_id))
+                user_profile.airavata_internal_user_id
+            )
+        )
 
     def _external_idp_user_info(self, user_id):
         # TODO(Phase C): external IDP claims were mirrored into the local Django
@@ -2081,8 +2260,7 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
         # still needed.
         return []
 
-    def _build_iam_user(self, user_profile, request, *, exists=None,
-                        groups=None):
+    def _build_iam_user(self, user_profile, request, *, exists=None, groups=None):
         """Compose an ``IAMUser`` from a proto ``UserProfile``.
 
         Resolves the composed parts (``DoesUserExist`` result, the user's groups
@@ -2104,33 +2282,35 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
             user_profile,
             airavata_user_profile_exists=exists,
             user_has_write_access=request.is_gateway_admin,
-            groups=to_jsonable(sharing_resources.wrap_groups(
-                request.airavata, groups)),
-            external_idp_user_info=self._external_idp_user_info(
-                user_profile.user_id),
+            groups=to_jsonable(sharing_resources.wrap_groups(request.airavata, groups)),
+            external_idp_user_info=self._external_idp_user_info(user_profile.user_id),
             user_profile_invalid_fields=self._user_profile_invalid_fields(
-                user_profile.user_id),
+                user_profile.user_id
+            ),
         )
 
     def list(self, request, *args, **kwargs):
-        search = request.GET.get('search', None)
+        search = request.GET.get("search", None)
 
         view = self
 
         class IAMUsersResultIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
-                return list(view._sdk().list_iam_users(
-                    request.airavata, offset=offset, limit=limit,
-                    search=search or ""))
-
-        queryset = IAMUsersResultIterator(
-            query_params=request.query_params.copy())
+                return list(
+                    view._sdk().list_iam_users(
+                        request.airavata,
+                        offset=offset,
+                        limit=limit,
+                        search=search or "",
+                    )
+                )
+
+        queryset = IAMUsersResultIterator(query_params=request.query_params.copy())
         page = self.paginate_queryset(queryset)
         if page is not None:
             data = [self._build_iam_user(u, request) for u in page]
             return self.get_paginated_response(data)
-        data = [self._build_iam_user(u, request)
-                for u in queryset.get_results()]
+        data = [self._build_iam_user(u, request) for u in queryset.get_results()]
         return web.Response(data)
 
     def retrieve(self, request, *args, **kwargs):
@@ -2141,19 +2321,21 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
     # APIResultIterator / write-path plumbing (serializer-backed)
 
     def get_list(self):
-        search = self.request.GET.get('search', None)
+        search = self.request.GET.get("search", None)
 
         convert_user_profile = self._convert_user_profile
 
         class IAMUsersResultIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
-                return map(convert_user_profile,
-                           iam_admin_client.get_users(offset, limit, search))
+                return map(
+                    convert_user_profile,
+                    iam_admin_client.get_users(offset, limit, search),
+                )
+
         return IAMUsersResultIterator(query_params=self.request.query_params.copy())
 
     def get_instance(self, lookup_value):
-        return self._convert_user_profile(
-            iam_admin_client.get_user(lookup_value))
+        return self._convert_user_profile(iam_admin_client.get_user(lookup_value))
 
     def update(self, request, *args, **kwargs):
         # Validate the camelCase body, apply the group-membership diff, then
@@ -2164,14 +2346,15 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
         except serializers.ValidationError as e:
             return web.Response(e.detail, status=web.status.HTTP_400_BAD_REQUEST)
         self._apply_group_diff(instance, data)
-        user_profile = iam_admin_client.get_user(data['userId'])
+        user_profile = iam_admin_client.get_user(data["userId"])
         return web.Response(self._build_iam_user(user_profile, request))
 
     def _apply_group_diff(self, instance, data):
         added_group_ids, removed_group_ids = serializers.iam_user_group_diff(
-            instance['groups'], data)
+            instance["groups"], data
+        )
         sharing = self.request.airavata.sharing
-        user_id = instance['airavataInternalUserId']
+        user_id = instance["airavataInternalUserId"]
         added_groups = []
         for group_id in added_group_ids:
             group = sharing.gm_get_group(group_id)
@@ -2179,29 +2362,30 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
             added_groups.append(group)
         if len(added_groups) > 0:
             user_profile = self.request.airavata.iam.get_user_profile_by_id(
-                data['userId'], settings.GATEWAY_ID)
+                data["userId"], settings.GATEWAY_ID
+            )
             signals.user_added_to_group.send(
                 sender=self.__class__,
                 user=user_profile,
                 groups=added_groups,
-                request=self.request)
+                request=self.request,
+            )
         for group_id in removed_group_ids:
             sharing.gm_remove_users_from_group([user_id], group_id)
 
     def perform_destroy(self, instance):
-        iam_admin_client.delete_user(instance['userId'])
+        iam_admin_client.delete_user(instance["userId"])
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def enable(self, request, user_id=None):
         iam_admin_client.enable_user(user_id)
         user_profile = iam_admin_client.get_user(user_id)
         return web.Response(self._build_iam_user(user_profile, request))
 
-    @web.action(methods=['put'], detail=False)
+    @web.action(methods=["put"], detail=False)
     def update_username(self, request):
         try:
-            old_username, new_username = serializers.parse_update_username(
-                request.data)
+            old_username, new_username = serializers.parse_update_username(request.data)
         except serializers.ValidationError as e:
             return web.Response(e.detail, status=web.status.HTTP_400_BAD_REQUEST)
         iam_admin_client.update_username(old_username, new_username)
@@ -2218,27 +2402,33 @@ class IAMUserViewSet(web.mixins.RetrieveModelMixin,
         from airavata_sdk.generated.org.apache.airavata.model.user import (
             user_profile_pb2,
         )
+
         Status = user_profile_pb2.Status
         airavata_user_profile_exists = self.request.airavata.iam.does_user_exist(
-            user_profile.user_id, self.gateway_id)
+            user_profile.user_id, self.gateway_id
+        )
         groups = []
         if airavata_user_profile_exists:
             groups = list(
                 self.request.airavata.sharing.gm_get_all_groups_user_belongs(
-                    user_profile.airavata_internal_user_id))
+                    user_profile.airavata_internal_user_id
+                )
+            )
         return {
-            'airavataInternalUserId': user_profile.airavata_internal_user_id,
-            'userId': user_profile.user_id,
-            'gatewayId': user_profile.gateway_id,
-            'email': user_profile.emails[0],
-            'firstName': user_profile.first_name,
-            'lastName': user_profile.last_name,
-            'enabled': user_profile.state == Status.ACTIVE,
-            'emailVerified': (user_profile.state == Status.CONFIRMED or
-                              user_profile.state == Status.ACTIVE),
-            'airavataUserProfileExists': airavata_user_profile_exists,
-            'creationTime': user_profile.creation_time,
-            'groups': groups
+            "airavataInternalUserId": user_profile.airavata_internal_user_id,
+            "userId": user_profile.user_id,
+            "gatewayId": user_profile.gateway_id,
+            "email": user_profile.emails[0],
+            "firstName": user_profile.first_name,
+            "lastName": user_profile.last_name,
+            "enabled": user_profile.state == Status.ACTIVE,
+            "emailVerified": (
+                user_profile.state == Status.CONFIRMED
+                or user_profile.state == Status.ACTIVE
+            ),
+            "airavataUserProfileExists": airavata_user_profile_exists,
+            "creationTime": user_profile.creation_time,
+            "groups": groups,
         }
 
 
@@ -2246,42 +2436,55 @@ class ExperimentStatisticsView(web.APIView):
     """Experiment-statistics resource. SDK returns the ``ExperimentStatistics``
     proto wholesale; the view nests it under ``results`` in a pagination envelope
     keyed on the proto's ``all_experiment_count``."""
+
     # TODO: restrict to only Admins or Read Only Admins group members
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import research_resources
+
         return research_resources
 
     def get(self, request, format=None):
-        if 'fromTime' in request.GET:
-            from_time = view_utils.convert_utc_iso8601_to_date(
-                request.GET['fromTime']).timestamp() * 1000
+        if "fromTime" in request.GET:
+            from_time = (
+                view_utils.convert_utc_iso8601_to_date(
+                    request.GET["fromTime"]
+                ).timestamp()
+                * 1000
+            )
         else:
-            from_time = (datetime.utcnow() -
-                         timedelta(days=7)).timestamp() * 1000
+            from_time = (datetime.now(UTC) - timedelta(days=7)).timestamp() * 1000
         from_time = int(from_time)
-        if 'toTime' in request.GET:
-            to_time = view_utils.convert_utc_iso8601_to_date(
-                request.GET['toTime']).timestamp() * 1000
+        if "toTime" in request.GET:
+            to_time = (
+                view_utils.convert_utc_iso8601_to_date(
+                    request.GET["toTime"]
+                ).timestamp()
+                * 1000
+            )
         else:
-            to_time = datetime.utcnow().timestamp() * 1000
+            to_time = datetime.now(UTC).timestamp() * 1000
         to_time = int(to_time)
-        username = request.GET.get('userName', None)
-        application_name = request.GET.get('applicationName', None)
-        resource_hostname = request.GET.get('resourceHostName', None)
-        limit = int(request.GET.get('limit', '50'))
-        offset = int(request.GET.get('offset', '0'))
+        username = request.GET.get("userName", None)
+        application_name = request.GET.get("applicationName", None)
+        resource_hostname = request.GET.get("resourceHostName", None)
+        limit = int(request.GET.get("limit", "50"))
+        offset = int(request.GET.get("offset", "0"))
 
         stats = self._sdk().get_experiment_statistics(
             request.airavata,
-            from_time=from_time, to_time=to_time,
-            user_name=username, application_name=application_name,
+            from_time=from_time,
+            to_time=to_time,
+            user_name=username,
+            application_name=application_name,
             resource_host_name=resource_hostname,
-            limit=limit, offset=offset)
+            limit=limit,
+            offset=offset,
+        )
 
         paginator = web.LimitOffsetPagination()
-        paginator.count = stats.all_experiment_count
+        paginator.count = stats.all_experiment_count  # ty: ignore[unresolved-attribute]  # DRF-shim pagination attr set dynamically
         paginator.limit = limit
         paginator.offset = offset
         paginator.request = request
@@ -2289,25 +2492,29 @@ class ExperimentStatisticsView(web.APIView):
         # paginated dict and flattens it to snake_case JSON.
         response = paginator.get_paginated_response(stats)
         # Also add limit and offset to the response
-        response.data['limit'] = limit
-        response.data['offset'] = offset
+        response.data["limit"] = limit
+        response.data["offset"] = offset
         return response
 
 
-class UnverifiedEmailUserViewSet(web.mixins.ListModelMixin,
-                                 web.mixins.RetrieveModelMixin,
-                                 GenericAPIBackedViewSet):
+class UnverifiedEmailUserViewSet(
+    web.mixins.ListModelMixin, web.mixins.RetrieveModelMixin, GenericAPIBackedViewSet
+):
     """Users whose email is not yet verified — a pydantic ``UnverifiedEmailUser``
     (a strict subset of ``IAMUser``). The ViewSet supplies
     ``request.is_gateway_admin``."""
 
     pagination_class = APIResultPagination
-    permission_classes = (web.IsAuthenticated, IsInAdminsGroupPermission,)
-    lookup_field = 'user_id'
+    permission_classes = (
+        web.IsAuthenticated,
+        IsInAdminsGroupPermission,
+    )
+    lookup_field = "user_id"
 
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import iam_resources
+
         return iam_resources
 
     def _build_unverified(self, user_profile, request):
@@ -2329,17 +2536,14 @@ class UnverifiedEmailUserViewSet(web.mixins.ListModelMixin,
         if page is not None:
             data = [self._build_unverified(u, request) for u in page]
             return self.get_paginated_response(data)
-        data = [self._build_unverified(u, request)
-                for u in queryset.get_results()]
+        data = [self._build_unverified(u, request) for u in queryset.get_results()]
         return web.Response(data)
 
     def retrieve(self, request, *args, **kwargs):
         user_id = self.kwargs[self.lookup_field]
-        users = self._get_unverified_email_user_profiles(
-            limit=1, username=user_id)
+        users = self._get_unverified_email_user_profiles(limit=1, username=user_id)
         if len(users) == 0:
-            raise Http404("No unverified email record found for user {}"
-                          .format(user_id))
+            raise Http404(f"No unverified email record found for user {user_id}")
         return web.Response(self._build_unverified(users[0], request))
 
     def get_list(self):
@@ -2348,19 +2552,17 @@ class UnverifiedEmailUserViewSet(web.mixins.ListModelMixin,
         class UnverifiedEmailUsersResultIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
                 return get_users(limit, offset)
+
         return UnverifiedEmailUsersResultIterator()
 
     def get_instance(self, lookup_value):
-        users = self._get_unverified_email_user_profiles(
-            limit=1, username=lookup_value)
+        users = self._get_unverified_email_user_profiles(limit=1, username=lookup_value)
         if len(users) == 0:
-            raise Http404("No unverified email record found for user {}"
-                          .format(lookup_value))
+            raise Http404(f"No unverified email record found for user {lookup_value}")
         else:
             return users[0]
 
-    def _get_unverified_email_user_profiles(
-            self, limit=-1, offset=0, username=None):
+    def _get_unverified_email_user_profiles(self, limit=-1, offset=0, username=None):
         # TODO(Phase C): self-registration email verification is now owned by
         # Keycloak (the local EmailVerification model was removed with the Django
         # account surface). Surface unverified-email users via a backend RPC /
@@ -2369,63 +2571,62 @@ class UnverifiedEmailUserViewSet(web.mixins.ListModelMixin,
 
 
 class LogRecordConsumer(web.APIView):
-
     def post(self, request, format=None):
         try:
             log_record = serializers.parse_log_record(request.data)
         except serializers.ValidationError as e:
             return web.Response(e.detail, status=web.status.HTTP_400_BAD_REQUEST)
-        log_level = getattr(logging, log_record['level'], None)
+        log_level = getattr(logging, log_record["level"], None)
         if log_level is not None:
-            stacktrace = "".join(
-                map(lambda a: "\n    " + a, log_record['stacktrace']))
-            log.log(log_level,
-                    "Frontend error: {}: {}\nstacktrace: {}".format(
-                        log_record['message'],
-                        json.dumps(log_record['details'], indent=4),
-                        stacktrace), extra={'request': request})
+            stacktrace = "".join("\n    " + a for a in log_record["stacktrace"])
+            log.log(
+                log_level,
+                "Frontend error: {}: {}\nstacktrace: {}".format(
+                    log_record["message"],
+                    json.dumps(log_record["details"], indent=4),
+                    stacktrace,
+                ),
+                extra={"request": request},
+            )
         return web.Response(serializers.render_log_record(log_record))
 
 
 class SettingsAPIView(web.APIView):
-
     def get(self, request, format=None):
-        return web.Response(serializers.settings_data(
-            settings.FILE_UPLOAD_MAX_FILE_SIZE,
-            settings.TUS_ENDPOINT,
-            settings.PGA_URL))
+        return web.Response(
+            serializers.settings_data(
+                settings.FILE_UPLOAD_MAX_FILE_SIZE,
+                settings.TUS_ENDPOINT,
+                settings.PGA_URL,
+            )
+        )
 
 
 class APIServerStatusCheckView(web.APIView):
-
     def get(self, request, format=None):
         try:
             request.airavata.research.get_user_projects(
                 gateway_id=settings.GATEWAY_ID,
                 user_name=request.user.username,
                 limit=1,
-                offset=0)
-            data = {
-                "apiServerUp": True
-            }
+                offset=0,
+            )
+            data = {"apiServerUp": True}
         except Exception as e:
-            log.debug("API server status check failed: {}".format(str(e)))
-            data = {
-                "apiServerUp": False
-            }
+            log.debug(f"API server status check failed: {e!s}")
+            data = {"apiServerUp": False}
         return web.Response(data)
 
 
 @web.api_view()
 def notebook_output_view(request):
-    provider_id = request.GET['provider-id']
-    experiment_id = request.GET['experiment-id']
-    experiment_output_name = request.GET['experiment-output-name']
-    data = output_views.generate_data(request,
-                                      provider_id,
-                                      experiment_output_name,
-                                      experiment_id)
-    return HttpResponse(data['output'])
+    provider_id = request.GET["provider-id"]
+    experiment_id = request.GET["experiment-id"]
+    experiment_output_name = request.GET["experiment-output-name"]
+    data = output_views.generate_data(
+        request, provider_id, experiment_output_name, experiment_id
+    )
+    return HttpResponse(data["output"])
 
 
 @web.api_view()
@@ -2439,7 +2640,7 @@ def image_output_view(request):
     data = _generate_output_view_data(request)
     # data should contain 'image' as a file-like object or raw bytes with the
     # file data and 'mime-type' with the images mimetype
-    data['image'] = base64.b64encode(data['image']).decode('utf-8')
+    data["image"] = base64.b64encode(data["image"]).decode("utf-8")
     return JsonResponse(data)
 
 
@@ -2451,20 +2652,23 @@ def link_output_view(request):
 
 def _generate_output_view_data(request):
     params = request.GET.copy()
-    provider_id = params.pop('provider-id')[0]
-    experiment_id = params.pop('experiment-id')[0]
-    experiment_output_name = params.pop('experiment-output-name')[0]
-    test_mode = ('test-mode' in params and params.pop('test-mode')[0] == "true")
-    return output_views.generate_data(request,
-                                      provider_id,
-                                      experiment_output_name,
-                                      experiment_id,
-                                      test_mode=test_mode,
-                                      **params.dict())
-
+    provider_id = params.pop("provider-id")[0]
+    experiment_id = params.pop("experiment-id")[0]
+    experiment_output_name = params.pop("experiment-output-name")[0]
+    test_mode = "test-mode" in params and params.pop("test-mode")[0] == "true"
+    return output_views.generate_data(
+        request,
+        provider_id,
+        experiment_output_name,
+        experiment_id,
+        test_mode=test_mode,
+        **params.dict(),
+    )
 
-class QueueSettingsCalculatorViewSet(web.mixins.ListModelMixin, web.mixins.RetrieveModelMixin, GenericAPIBackedViewSet):
 
+class QueueSettingsCalculatorViewSet(
+    web.mixins.ListModelMixin, web.mixins.RetrieveModelMixin, GenericAPIBackedViewSet
+):
     def get_list(self):
         return queue_settings.get_all()
 
@@ -2476,28 +2680,34 @@ class QueueSettingsCalculatorViewSet(web.mixins.ListModelMixin, web.mixins.Retri
         return calc[0]
 
     def list(self, request, *args, **kwargs):
-        return web.Response([serializers.queue_settings_calculator_data(c)
-                         for c in self.get_list()])
+        return web.Response(
+            [serializers.queue_settings_calculator_data(c) for c in self.get_list()]
+        )
 
     def retrieve(self, request, *args, **kwargs):
-        return web.Response(serializers.queue_settings_calculator_data(
-            self.get_object()))
+        return web.Response(
+            serializers.queue_settings_calculator_data(self.get_object())
+        )
 
-    @web.action(methods=['post'], detail=True)
+    @web.action(methods=["post"], detail=True)
     def calculate(self, request, pk=None):
         from airavata_sdk.helpers import research_resources
+
         data = request.data if isinstance(request.data, dict) else {}
         result = {}
         # Build the proto ExperimentModel from the request; ignore a malformed
         # body (likely a late-initialization partial) and return empty settings.
         try:
             experiment_model = research_resources.build_experiment(
-                request.airavata, data)
+                request.airavata, data
+            )
         except Exception:
-            log.debug("Ignoring invalid experiment model for queue calculation",
-                      exc_info=True)
+            log.debug(
+                "Ignoring invalid experiment model for queue calculation", exc_info=True
+            )
             experiment_model = None
         if experiment_model is not None:
             result = queue_settings.calculate_queue_settings(
-                pk, request, experiment_model)
+                pk, request, experiment_model
+            )
         return web.Response(result)
diff --git a/airavata-django-portal/django_airavata/apps/api/web.py b/airavata-django-portal/django_airavata/apps/api/web.py
index d53c0de8b..03a661915 100644
--- a/airavata-django-portal/django_airavata/apps/api/web.py
+++ b/airavata-django-portal/django_airavata/apps/api/web.py
@@ -43,6 +43,7 @@ log = logging.getLogger(__name__)
 # status
 # ---------------------------------------------------------------------------
 
+
 class _Status:
     """Namespace of the HTTP status constants callers reference as
     ``status.HTTP_*`` (mirrors ``rest_framework.status``)."""
@@ -66,6 +67,7 @@ status = _Status()
 # Response
 # ---------------------------------------------------------------------------
 
+
 class Response(HttpResponse):
     """An ``HttpResponse`` initialized with *unrendered* data (like DRF).
 
@@ -84,7 +86,7 @@ class Response(HttpResponse):
 
     def __init__(self, data=None, status=200, headers=None, content_type=None):
         # Initialize the HttpResponse with an empty body; do NOT serialize yet.
-        super().__init__(content=b'', status=status, content_type=content_type)
+        super().__init__(content=b"", status=status, content_type=content_type)
         self.data = data
         self._is_rendered = False
         if headers:
@@ -95,10 +97,9 @@ class Response(HttpResponse):
     def rendered_content(self):
         """The JSON-encoded body bytes for ``self.data`` (empty for 204/None)."""
         if self.status_code == status.HTTP_204_NO_CONTENT or self.data is None:
-            return b''
-        self['Content-Type'] = 'application/json'
-        return json.dumps(
-            to_jsonable(self.data), cls=DjangoJSONEncoder).encode('utf-8')
+            return b""
+        self["Content-Type"] = "application/json"
+        return json.dumps(to_jsonable(self.data), cls=DjangoJSONEncoder).encode("utf-8")
 
     def render(self):
         """Materialize ``rendered_content`` into ``self.content`` (idempotent).
@@ -130,6 +131,7 @@ class Response(HttpResponse):
 # Exceptions
 # ---------------------------------------------------------------------------
 
+
 class ParseError(Exception):
     """Raised for malformed request input; maps to HTTP 400."""
 
@@ -161,7 +163,7 @@ class ValidationError(Exception):
 # Permissions
 # ---------------------------------------------------------------------------
 
-SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
+SAFE_METHODS = ("GET", "HEAD", "OPTIONS")
 
 
 class OperationHolderMixin:
@@ -198,6 +200,7 @@ class _OperandHolder(OperationHolderMixin):
 
 class BasePermissionMetaclass(OperationHolderMixin, type):
     """Metaclass so the operators work on permission *classes* directly."""
+
     pass
 
 
@@ -226,12 +229,14 @@ class AND:
         self.op2 = op2
 
     def has_permission(self, request, view):
-        return (self.op1.has_permission(request, view) and
-                self.op2.has_permission(request, view))
+        return self.op1.has_permission(request, view) and self.op2.has_permission(
+            request, view
+        )
 
     def has_object_permission(self, request, view, obj):
-        return (self.op1.has_object_permission(request, view, obj) and
-                self.op2.has_object_permission(request, view, obj))
+        return self.op1.has_object_permission(
+            request, view, obj
+        ) and self.op2.has_object_permission(request, view, obj)
 
 
 class OR:
@@ -240,17 +245,19 @@ class OR:
         self.op2 = op2
 
     def has_permission(self, request, view):
-        return (self.op1.has_permission(request, view) or
-                self.op2.has_permission(request, view))
+        return self.op1.has_permission(request, view) or self.op2.has_permission(
+            request, view
+        )
 
     def has_object_permission(self, request, view, obj):
         # Mirror DRF: an OR short-circuits at the request level — if op1 already
         # granted request-level access, object-level access is also granted.
         return (
-            (self.op1.has_permission(request, view) and
-             self.op1.has_object_permission(request, view, obj)) or
-            (self.op2.has_permission(request, view) and
-             self.op2.has_object_permission(request, view, obj))
+            self.op1.has_permission(request, view)
+            and self.op1.has_object_permission(request, view, obj)
+        ) or (
+            self.op2.has_permission(request, view)
+            and self.op2.has_object_permission(request, view, obj)
         )
 
 
@@ -279,14 +286,14 @@ def _cls_invert(cls):
     return _OperandHolder(NOT, cls)
 
 
-OperationHolderMixin.__or__ = lambda self, other: _OperandHolder(OR, self, other)
-OperationHolderMixin.__and__ = lambda self, other: _OperandHolder(AND, self, other)
-OperationHolderMixin.__invert__ = lambda self: _OperandHolder(NOT, self)
+OperationHolderMixin.__or__ = lambda self, other: _OperandHolder(OR, self, other)  # ty: ignore[invalid-assignment]  # intentional monkeypatch so class-level operators return _OperandHolder
+OperationHolderMixin.__and__ = lambda self, other: _OperandHolder(AND, self, other)  # ty: ignore[invalid-assignment]  # intentional monkeypatch so class-level operators return _OperandHolder
+OperationHolderMixin.__invert__ = lambda self: _OperandHolder(NOT, self)  # ty: ignore[invalid-assignment]  # intentional monkeypatch so class-level operators return _OperandHolder
 
 
 class IsAuthenticated(BasePermission):
     def has_permission(self, request, view):
-        user = getattr(request, 'user', None)
+        user = getattr(request, "user", None)
         return bool(user and user.is_authenticated)
 
 
@@ -313,11 +320,13 @@ GRPC_STATUS_TO_HTTP = {
 
 class NotAuthenticated(Exception):
     """Permission failure for an unauthenticated request (maps to 401)."""
+
     pass
 
 
 class PermissionDenied(Exception):
     """Permission failure for an authenticated request (maps to 403)."""
+
     pass
 
 
@@ -330,47 +339,58 @@ def exception_to_response(exc, request=None):
         if code == grpc.StatusCode.UNAVAILABLE:
             log.warning("gRPC UNAVAILABLE", exc_info=exc)
             return Response(
-                {'detail': detail, 'apiServerDown': True},
-                status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+                {"detail": detail, "apiServerDown": True},
+                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            )
         http_status = GRPC_STATUS_TO_HTTP.get(
-            code, status.HTTP_500_INTERNAL_SERVER_ERROR)
+            code, status.HTTP_500_INTERNAL_SERVER_ERROR
+        )
         if http_status >= 500:
             log.error("gRPC error %s", code, exc_info=exc)
         else:
             log.warning("gRPC error %s", code, exc_info=exc)
-        return Response({'detail': detail}, status=http_status)
+        return Response({"detail": detail}, status=http_status)
 
     if isinstance(exc, (ObjectDoesNotExist,)):
         log.warning("ObjectDoesNotExist", exc_info=exc)
-        return Response({'detail': str(exc)}, status=status.HTTP_404_NOT_FOUND)
+        return Response({"detail": str(exc)}, status=status.HTTP_404_NOT_FOUND)
 
     if isinstance(exc, Http404):
-        return Response({'detail': str(exc) or "Not found."},
-                        status=status.HTTP_404_NOT_FOUND)
+        return Response(
+            {"detail": str(exc) or "Not found."}, status=status.HTTP_404_NOT_FOUND
+        )
 
     if isinstance(exc, NotAuthenticated):
-        return Response({'detail': str(exc) or "Authentication credentials were not provided.",
-                         'is_authenticated': False},
-                        status=status.HTTP_401_UNAUTHORIZED)
+        return Response(
+            {
+                "detail": str(exc) or "Authentication credentials were not provided.",
+                "is_authenticated": False,
+            },
+            status=status.HTTP_401_UNAUTHORIZED,
+        )
 
     if isinstance(exc, PermissionDenied):
-        return Response({'detail': str(exc) or "You do not have permission to perform this action."},
-                        status=status.HTTP_403_FORBIDDEN)
+        return Response(
+            {
+                "detail": str(exc)
+                or "You do not have permission to perform this action."
+            },
+            status=status.HTTP_403_FORBIDDEN,
+        )
 
     if isinstance(exc, (ParseError, ValidationError)):
-        return Response({'detail': exc.detail},
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response({"detail": exc.detail}, status=status.HTTP_400_BAD_REQUEST)
 
     # Generic handler
     log.error("API exception", exc_info=exc)
-    return Response({'detail': str(exc)},
-                    status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+    return Response({"detail": str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
 
 # ---------------------------------------------------------------------------
 # Query-param helpers (ports of DRF's ~10-line impls).
 # ---------------------------------------------------------------------------
 
+
 def replace_query_param(url, key, val):
     """Return ``url`` with the query parameter ``key`` set to ``val``."""
     (scheme, netloc, path, query, fragment) = parse.urlsplit(str(url))
@@ -393,6 +413,7 @@ def remove_query_param(url, key):
 # Rendering helpers shared by APIView / ViewSet dispatch.
 # ---------------------------------------------------------------------------
 
+
 def _render_response(result):
     """Normalize a handler return value into an ``HttpResponse`` to return.
 
@@ -416,6 +437,7 @@ def _instantiate_permissions(permission_classes):
 # APIView
 # ---------------------------------------------------------------------------
 
+
 class APIView(View):
     """Plain-Django reimplementation of ``rest_framework.views.APIView``.
 
@@ -429,14 +451,22 @@ class APIView(View):
     permission_classes = [IsAuthenticated]
 
     # Subclasses (and the ViewSet routing layer) may override.
-    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head',
-                         'options', 'trace']
+    http_method_names = [
+        "get",
+        "post",
+        "put",
+        "patch",
+        "delete",
+        "head",
+        "options",
+        "trace",
+    ]
 
     def get_permissions(self):
         return _instantiate_permissions(self.permission_classes)
 
     def permission_denied(self, request, message=None):
-        user = getattr(request, 'user', None)
+        user = getattr(request, "user", None)
         if not (user and user.is_authenticated):
             raise NotAuthenticated(message)
         raise PermissionDenied(message)
@@ -445,13 +475,15 @@ class APIView(View):
         for permission in self.get_permissions():
             if not permission.has_permission(request, self):
                 self.permission_denied(
-                    request, message=getattr(permission, 'message', None))
+                    request, message=getattr(permission, "message", None)
+                )
 
     def check_object_permissions(self, request, obj):
         for permission in self.get_permissions():
             if not permission.has_object_permission(request, self, obj):
                 self.permission_denied(
-                    request, message=getattr(permission, 'message', None))
+                    request, message=getattr(permission, "message", None)
+                )
 
     def initial(self, request, *args, **kwargs):
         self.check_permissions(request)
@@ -462,8 +494,9 @@ class APIView(View):
         self.kwargs = kwargs
         try:
             self.initial(request, *args, **kwargs)
-            handler = getattr(self, request.method.lower(),
-                              self.http_method_not_allowed)
+            handler = getattr(
+                self, request.method.lower(), self.http_method_not_allowed
+            )
             result = handler(request, *args, **kwargs)
             return _render_response(result)
         except Exception as exc:
@@ -471,14 +504,15 @@ class APIView(View):
 
     def http_method_not_allowed(self, request, *args, **kwargs):
         return Response(
-            {'detail': f'Method "{request.method}" not allowed.'},
-            status=405)
+            {"detail": f'Method "{request.method}" not allowed.'}, status=405
+        )
 
 
 # ---------------------------------------------------------------------------
 # Serializer-less GenericViewSet / ViewSet + model mixins
 # ---------------------------------------------------------------------------
 
+
 class ViewSetMixin:
     """Reproduces DRF ``ViewSetMixin.as_view(actions)`` semantics: map each HTTP
     method to a named action, set ``self.action``/``self.action_map``, then
@@ -490,13 +524,14 @@ class ViewSetMixin:
         if not actions:
             raise TypeError(
                 "The `actions` argument must be provided when calling "
-                "`.as_view()` on a ViewSet.")
+                "`.as_view()` on a ViewSet."
+            )
 
         def view(request, *args, **kwargs):
             self = cls(**initkwargs)
-            if 'get' in actions and 'head' not in actions:
-                actions['head'] = actions['get']
-            self.action_map = actions
+            if "get" in actions and "head" not in actions:
+                actions["head"] = actions["get"]
+            self.action_map = actions  # ty: ignore[unresolved-attribute]  # DRF shim: action_map set dynamically on the viewset instance
             for method, action in actions.items():
                 handler = getattr(self, action)
                 setattr(self, method, handler)
@@ -505,17 +540,17 @@ class ViewSetMixin:
             self.kwargs = kwargs
             return self.dispatch(request, *args, **kwargs)
 
-        view.cls = cls
-        view.initkwargs = initkwargs
-        view.actions = actions
+        view.cls = cls  # ty: ignore[unresolved-attribute]  # DRF shim: dynamic function attribute (mirrors DRF as_view)
+        view.initkwargs = initkwargs  # ty: ignore[unresolved-attribute]  # DRF shim: dynamic function attribute (mirrors DRF as_view)
+        view.actions = actions  # ty: ignore[unresolved-attribute]  # DRF shim: dynamic function attribute (mirrors DRF as_view)
         return view
 
     def initialize_request(self, request):
         method = request.method.lower()
-        if method == 'options':
-            self.action = 'metadata'
+        if method == "options":
+            self.action = "metadata"
         else:
-            self.action = self.action_map.get(method)
+            self.action = self.action_map.get(method)  # ty: ignore[unresolved-attribute]  # DRF shim: action_map set on the instance by as_view()
 
     def dispatch(self, request, *args, **kwargs):
         self.request = request
@@ -523,9 +558,12 @@ class ViewSetMixin:
         self.kwargs = kwargs
         self.initialize_request(request)
         try:
-            self.initial(request, *args, **kwargs)
-            handler = getattr(self, request.method.lower(),
-                              self.http_method_not_allowed)
+            self.initial(request, *args, **kwargs)  # ty: ignore[unresolved-attribute]  # mixin: always composed with APIView (ViewSet/GenericViewSet)
+            handler = getattr(
+                self,
+                request.method.lower(),
+                self.http_method_not_allowed,  # ty: ignore[unresolved-attribute]  # mixin: always composed with APIView (ViewSet/GenericViewSet)
+            )
             result = handler(request, *args, **kwargs)
             return _render_response(result)
         except Exception as exc:
@@ -533,12 +571,15 @@ class ViewSetMixin:
 
     @classmethod
     def get_extra_actions(cls):
-        return [method for _, method in
-                getmembers(cls, lambda attr: hasattr(attr, 'mapping'))]
+        return [
+            method
+            for _, method in getmembers(cls, lambda attr: hasattr(attr, "mapping"))
+        ]
 
 
 class ViewSet(ViewSetMixin, APIView):
     """A viewset with no default actions (matches DRF ``ViewSet``)."""
+
     pass
 
 
@@ -549,9 +590,9 @@ class GenericViewSet(ViewSetMixin, APIView):
     """
 
     # Match GenericAPIBackedViewSet's relaxed regex (Airavata ids contain '.').
-    lookup_field = 'pk'
+    lookup_field = "pk"
     lookup_url_kwarg = None
-    lookup_value_regex = '[^/]+'
+    lookup_value_regex = "[^/]+"
 
     serializer_class = None
     queryset = None
@@ -580,11 +621,16 @@ class GenericViewSet(ViewSetMixin, APIView):
         lookup_value = self.kwargs[lookup_url_kwarg]
         try:
             obj = queryset.get(**{self.lookup_field: lookup_value})
-        except (ObjectDoesNotExist, ValueError, TypeError,
-                DjangoValidationError):
+        except (
+            ObjectDoesNotExist,
+            ValueError,
+            TypeError,
+            DjangoValidationError,
+        ) as err:
             raise Http404(
                 f"No {getattr(queryset, 'model', type(self)).__name__} matches "
-                "the given query.")
+                "the given query."
+            ) from err
         self.check_object_permissions(self.request, obj)
         return obj
 
@@ -593,15 +639,16 @@ class GenericViewSet(ViewSetMixin, APIView):
         assert self.serializer_class is not None, (
             f"'{self.__class__.__name__}' should either include a "
             "`serializer_class` attribute, or override the "
-            "`get_serializer_class()` method.")
+            "`get_serializer_class()` method."
+        )
         return self.serializer_class
 
     def get_serializer_context(self):
-        return {'request': self.request, 'view': self}
+        return {"request": self.request, "view": self}
 
     def get_serializer(self, *args, **kwargs):
         serializer_class = self.get_serializer_class()
-        kwargs.setdefault('context', self.get_serializer_context())
+        kwargs.setdefault("context", self.get_serializer_context())
         return serializer_class(*args, **kwargs)
 
     # -- pagination -------------------------------------------------------
@@ -617,8 +664,7 @@ class GenericViewSet(ViewSetMixin, APIView):
     def paginate_queryset(self, queryset):
         if self.paginator is None:
             return None
-        return self.paginator.paginate_queryset(
-            queryset, self.request, view=self)
+        return self.paginator.paginate_queryset(queryset, self.request, view=self)
 
     def get_paginated_response(self, data):
         return self.paginator.get_paginated_response(data)
@@ -626,27 +672,32 @@ class GenericViewSet(ViewSetMixin, APIView):
 
 # -- model mixins (DRF default list/retrieve/create/update/destroy) -------
 
+
+# These mixins are always composed with GenericViewSet, which supplies
+# filter_queryset/get_queryset/paginate_queryset/get_serializer/
+# get_paginated_response/get_object. ty analyzes each mixin standalone, so those
+# attribute accesses read as unresolved here even though they resolve at runtime.
 class ListModelMixin:
     def list(self, request, *args, **kwargs):
-        queryset = self.filter_queryset(self.get_queryset())
-        page = self.paginate_queryset(queryset)
+        queryset = self.filter_queryset(self.get_queryset())  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
+        page = self.paginate_queryset(queryset)  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
         if page is not None:
-            serializer = self.get_serializer(page, many=True)
-            return self.get_paginated_response(serializer.data)
-        serializer = self.get_serializer(queryset, many=True)
+            serializer = self.get_serializer(page, many=True)  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
+            return self.get_paginated_response(serializer.data)  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
+        serializer = self.get_serializer(queryset, many=True)  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
         return Response(serializer.data)
 
 
 class RetrieveModelMixin:
     def retrieve(self, request, *args, **kwargs):
-        instance = self.get_object()
-        serializer = self.get_serializer(instance)
+        instance = self.get_object()  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
+        serializer = self.get_serializer(instance)  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
         return Response(serializer.data)
 
 
 class CreateModelMixin:
     def create(self, request, *args, **kwargs):
-        serializer = self.get_serializer(data=request.data)
+        serializer = self.get_serializer(data=request.data)  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
         serializer.is_valid(raise_exception=True)
         self.perform_create(serializer)
         return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -657,10 +708,9 @@ class CreateModelMixin:
 
 class UpdateModelMixin:
     def update(self, request, *args, **kwargs):
-        partial = kwargs.pop('partial', False)
-        instance = self.get_object()
-        serializer = self.get_serializer(
-            instance, data=request.data, partial=partial)
+        partial = kwargs.pop("partial", False)
+        instance = self.get_object()  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
+        serializer = self.get_serializer(instance, data=request.data, partial=partial)  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
         serializer.is_valid(raise_exception=True)
         self.perform_update(serializer)
         return Response(serializer.data)
@@ -669,13 +719,13 @@ class UpdateModelMixin:
         serializer.save()
 
     def partial_update(self, request, *args, **kwargs):
-        kwargs['partial'] = True
+        kwargs["partial"] = True
         return self.update(request, *args, **kwargs)
 
 
 class DestroyModelMixin:
     def destroy(self, request, *args, **kwargs):
-        instance = self.get_object()
+        instance = self.get_object()  # ty: ignore[unresolved-attribute]  # provided by GenericViewSet at composition
         self.perform_destroy(instance)
         return Response(status=status.HTTP_204_NO_CONTENT)
 
@@ -685,20 +735,23 @@ class DestroyModelMixin:
 
 # -- composed viewsets (DRF ModelViewSet / ReadOnlyModelViewSet) ----------
 
-class ModelViewSet(CreateModelMixin,
-                   RetrieveModelMixin,
-                   UpdateModelMixin,
-                   DestroyModelMixin,
-                   ListModelMixin,
-                   GenericViewSet):
+
+class ModelViewSet(
+    CreateModelMixin,
+    RetrieveModelMixin,
+    UpdateModelMixin,
+    DestroyModelMixin,
+    ListModelMixin,
+    GenericViewSet,
+):
     """All CRUD actions (matches DRF ``ModelViewSet``)."""
+
     pass
 
 
-class ReadOnlyModelViewSet(RetrieveModelMixin,
-                           ListModelMixin,
-                           GenericViewSet):
+class ReadOnlyModelViewSet(RetrieveModelMixin, ListModelMixin, GenericViewSet):
     """``retrieve()`` + ``list()`` only (matches DRF ``ReadOnlyModelViewSet``)."""
+
     pass
 
 
@@ -731,6 +784,7 @@ viewsets = _Viewsets()
 # @action / @api_view decorators
 # ---------------------------------------------------------------------------
 
+
 def action(detail=False, methods=None, url_path=None, url_name=None, **kwargs):
     """Mark a viewset method as a routable extra action.
 
@@ -740,14 +794,14 @@ def action(detail=False, methods=None, url_path=None, url_name=None, **kwargs):
     attributes, so :func:`route` can route to it (mirrors ``rest_framework``'s
     ``@action``).
     """
-    methods = ['get'] if methods is None else methods
+    methods = ["get"] if methods is None else methods
     methods = [m.lower() for m in methods]
 
     def decorator(func):
-        func.mapping = {m: func.__name__ for m in methods}
+        func.mapping = dict.fromkeys(methods, func.__name__)
         func.detail = detail
         func.url_path = url_path if url_path else func.__name__
-        func.url_name = url_name if url_name else func.__name__.replace('_', '-')
+        func.url_name = url_name if url_name else func.__name__.replace("_", "-")
         func.kwargs = kwargs
         return func
 
@@ -761,7 +815,7 @@ def api_view(http_method_names=None):
     mapping, ``IsAuthenticated`` by default; ``request.data``/``query_params``
     come from the request-augmentation middleware).
     """
-    http_method_names = ['GET'] if http_method_names is None else http_method_names
+    http_method_names = ["GET"] if http_method_names is None else http_method_names
     allowed = [m.lower() for m in http_method_names]
 
     def decorator(func):
@@ -776,10 +830,11 @@ def api_view(http_method_names=None):
 
         # @permission_classes([...]) (below) may override this.
         WrappedAPIView.permission_classes = getattr(
-            func, 'permission_classes', APIView.permission_classes)
+            func, "permission_classes", APIView.permission_classes
+        )
         WrappedAPIView.__name__ = func.__name__
         WrappedAPIView.__doc__ = func.__doc__
-        WrappedAPIView.func = staticmethod(func)
+        WrappedAPIView.func = staticmethod(func)  # ty: ignore[unresolved-attribute]  # dynamic class attribute (mirrors DRF @api_view)
         return WrappedAPIView.as_view()
 
     return decorator
@@ -789,9 +844,11 @@ def permission_classes(permission_classes):
     """``@permission_classes([...])`` for ``@api_view`` functions. Apply it
     *above* ``@api_view`` (DRF order); it stashes the classes the wrapper reads.
     """
+
     def decorator(func):
         func.permission_classes = permission_classes
         return func
+
     return decorator
 
 
@@ -799,6 +856,7 @@ def permission_classes(permission_classes):
 # LimitOffsetPagination (ports view_utils.APIResultPagination's base).
 # ---------------------------------------------------------------------------
 
+
 class LimitOffsetPagination:
     """Limit/offset pagination over the request query params.
 
@@ -810,8 +868,8 @@ class LimitOffsetPagination:
     """
 
     default_limit = 10
-    limit_query_param = 'limit'
-    offset_query_param = 'offset'
+    limit_query_param = "limit"
+    offset_query_param = "offset"
     max_limit = None
 
     def get_limit(self, request):
@@ -838,16 +896,20 @@ class LimitOffsetPagination:
             return None
         self.offset = self.get_offset(request)
         self.request = request
-        return list(queryset[self.offset:self.offset + self.limit])
+        return list(queryset[self.offset : self.offset + self.limit])
 
     def get_paginated_response(self, data):
-        return Response(OrderedDict([
-            ('next', self.get_next_link()),
-            ('previous', self.get_previous_link()),
-            ('results', data),
-            ('limit', self.limit),
-            ('offset', self.offset),
-        ]))
+        return Response(
+            OrderedDict(
+                [
+                    ("next", self.get_next_link()),
+                    ("previous", self.get_previous_link()),
+                    ("results", data),
+                    ("limit", self.limit),
+                    ("offset", self.offset),
+                ]
+            )
+        )
 
     def get_next_link(self):
         url = self.request.build_absolute_uri()
@@ -895,28 +957,32 @@ permissions = _Permissions()
 # The fixed list/detail routes, mirroring DRF SimpleRouter.routes (minus the
 # DynamicRoute placeholders, which are expanded per @action below).
 _LIST_ROUTE = {
-    'url': r'^{prefix}/$',
-    'mapping': {'get': 'list', 'post': 'create'},
-    'name': '{basename}-list',
-    'detail': False,
+    "url": r"^{prefix}/$",
+    "mapping": {"get": "list", "post": "create"},
+    "name": "{basename}-list",
+    "detail": False,
 }
 _DETAIL_ROUTE = {
-    'url': r'^{prefix}/{lookup}/$',
-    'mapping': {'get': 'retrieve', 'put': 'update',
-                'patch': 'partial_update', 'delete': 'destroy'},
-    'name': '{basename}-detail',
-    'detail': True,
+    "url": r"^{prefix}/{lookup}/$",
+    "mapping": {
+        "get": "retrieve",
+        "put": "update",
+        "patch": "partial_update",
+        "delete": "destroy",
+    },
+    "name": "{basename}-detail",
+    "detail": True,
 }
 
 
 def _get_lookup_regex(viewset):
-    lookup_field = getattr(viewset, 'lookup_field', 'pk')
-    lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
+    lookup_field = getattr(viewset, "lookup_field", "pk")
+    lookup_url_kwarg = getattr(viewset, "lookup_url_kwarg", None) or lookup_field
     # DRF's SimpleRouter default value pattern is '[^/.]+'; our GenericViewSet
     # base overrides it to '[^/]+'. Use the viewset's value, falling back to the
     # DRF default for viewsets that don't declare one.
-    lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
-    return f'(?P<{lookup_url_kwarg}>{lookup_value})'
+    lookup_value = getattr(viewset, "lookup_value_regex", "[^/.]+")
+    return f"(?P<{lookup_url_kwarg}>{lookup_value})"
 
 
 def _method_map(viewset, mapping):
@@ -929,8 +995,7 @@ def _method_map(viewset, mapping):
 
 
 def _extra_actions(viewset):
-    return [m for _, m in
-            getmembers(viewset, lambda attr: hasattr(attr, 'mapping'))]
+    return [m for _, m in getmembers(viewset, lambda attr: hasattr(attr, "mapping"))]
 
 
 def route(prefix, viewset, basename, lookup_field=None):
@@ -947,7 +1012,7 @@ def route(prefix, viewset, basename, lookup_field=None):
     if lookup_field is not None:
         # Temporary shadow so the lookup regex uses the override without mutating
         # the viewset class.
-        original = getattr(viewset, 'lookup_field', 'pk')
+        original = getattr(viewset, "lookup_field", "pk")
         viewset.lookup_field = lookup_field
         try:
             lookup = _get_lookup_regex(viewset)
@@ -965,32 +1030,36 @@ def route(prefix, viewset, basename, lookup_field=None):
     specs = []
     specs.append(dict(_LIST_ROUTE))
     for a in list_actions:
-        specs.append({
-            'url': r'^{prefix}/' + _escape(a.url_path) + r'/$',
-            'mapping': dict(a.mapping),
-            'name': '{basename}-' + a.url_name,
-            'detail': False,
-        })
+        specs.append(
+            {
+                "url": r"^{prefix}/" + _escape(a.url_path) + r"/$",
+                "mapping": dict(a.mapping),
+                "name": "{basename}-" + a.url_name,
+                "detail": False,
+            }
+        )
     specs.append(dict(_DETAIL_ROUTE))
     for a in detail_actions:
-        specs.append({
-            'url': r'^{prefix}/{lookup}/' + _escape(a.url_path) + r'/$',
-            'mapping': dict(a.mapping),
-            'name': '{basename}-' + a.url_name,
-            'detail': True,
-        })
+        specs.append(
+            {
+                "url": r"^{prefix}/{lookup}/" + _escape(a.url_path) + r"/$",
+                "mapping": dict(a.mapping),
+                "name": "{basename}-" + a.url_name,
+                "detail": True,
+            }
+        )
 
     urls = []
     for spec in specs:
-        mapping = _method_map(viewset, spec['mapping'])
+        mapping = _method_map(viewset, spec["mapping"])
         if not mapping:
             continue
-        regex = spec['url'].format(prefix=prefix, lookup=lookup)
-        view = viewset.as_view(mapping, basename=basename, detail=spec['detail'])
-        name = spec['name'].format(basename=basename)
+        regex = spec["url"].format(prefix=prefix, lookup=lookup)  # ty: ignore[unresolved-attribute]  # heterogeneous spec dict: "url" is always a str
+        view = viewset.as_view(mapping, basename=basename, detail=spec["detail"])
+        name = spec["name"].format(basename=basename)  # ty: ignore[unresolved-attribute]  # heterogeneous spec dict: "name" is always a str
         urls.append(re_path(regex, view, name=name))
     return urls
 
 
 def _escape(url_path):
-    return url_path.replace('{', '{{').replace('}', '}}')
+    return url_path.replace("{", "{{").replace("}", "}}")
diff --git a/airavata-django-portal/django_airavata/apps/auth/apps.py b/airavata-django-portal/django_airavata/apps/auth/apps.py
index cca02f9f2..eb7b7e84d 100644
--- a/airavata-django-portal/django_airavata/apps/auth/apps.py
+++ b/airavata-django-portal/django_airavata/apps/auth/apps.py
@@ -2,8 +2,8 @@ from django.apps import AppConfig
 
 
 class AuthConfig(AppConfig):
-    name = 'django_airavata.apps.auth'
-    label = 'django_airavata_auth'
+    name = "django_airavata.apps.auth"
+    label = "django_airavata_auth"
 
     def ready(self):
         from . import signals  # noqa
diff --git a/airavata-django-portal/django_airavata/apps/auth/decorators.py b/airavata-django-portal/django_airavata/apps/auth/decorators.py
new file mode 100644
index 000000000..80af215b4
--- /dev/null
+++ b/airavata-django-portal/django_airavata/apps/auth/decorators.py
@@ -0,0 +1,39 @@
+"""DB-free auth decorators.
+
+``django.contrib.auth.decorators.login_required`` cannot be used once
+``django.contrib.auth`` / ``django.contrib.contenttypes`` are removed from
+``INSTALLED_APPS``: its anonymous-redirect path lazily imports
+``django.contrib.auth.views`` -> ``forms`` -> ``models`` ->
+``contenttypes.models``, which raises because those model classes have no
+installed app. Identity here comes from the verified Keycloak token, so this is
+a minimal ``request.user.is_authenticated`` check that redirects to
+``settings.LOGIN_URL`` with the usual ``?next=`` parameter.
+"""
+
+from functools import wraps
+from urllib.parse import urlparse, urlunparse
+
+from django.conf import settings
+from django.http import HttpResponseRedirect, QueryDict
+from django.shortcuts import resolve_url
+
+
+def login_required(view_func=None, redirect_field_name="next", login_url=None):
+    def decorator(view):
+        @wraps(view)
+        def _wrapped(request, *args, **kwargs):
+            if getattr(request.user, "is_authenticated", False):
+                return view(request, *args, **kwargs)
+            resolved_url = resolve_url(login_url or settings.LOGIN_URL)
+            parts = list(urlparse(resolved_url))
+            if redirect_field_name:
+                querystring = QueryDict(parts[4], mutable=True)
+                querystring[redirect_field_name] = request.get_full_path()
+                parts[4] = querystring.urlencode(safe="/")
+            return HttpResponseRedirect(urlunparse(parts))
+
+        return _wrapped
+
+    if view_func:
+        return decorator(view_func)
+    return decorator
diff --git a/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py b/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py
index c080409b6..afa384da3 100644
--- a/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py
+++ b/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py
@@ -1,14 +1,13 @@
 """IAM admin operations via the gRPC ``iam`` facade.
 
-These operations (username availability, user registration, enable/reset) run in
-unauthenticated contexts — account creation, email verification, password reset —
-so they use a Keycloak **service-account** token rather than a logged-in user's
-token. Each call builds a short-lived ``AiravataClient`` scoped to that token and
-talks to the gRPC ``iam`` facade; callers consume the returned protobuf
+These operations run in admin/service contexts (e.g. the user-management admin
+views), so they use a Keycloak **service-account** token rather than a logged-in
+user's token. Each call builds a short-lived ``AiravataClient`` scoped to that
+token and talks to the gRPC ``iam`` facade; callers consume the returned protobuf
 ``UserProfile`` directly (``user_id``/``first_name``/``last_name``/``emails``).
 
-``update_user``/``update_username`` talk to the Keycloak admin REST API directly
-(not the gRPC backend) and are unchanged.
+``update_username`` talks to the Keycloak admin REST API directly (not the gRPC
+backend).
 """
 
 import logging
@@ -35,7 +34,8 @@ def _iam():
     """
     access_token = utils.get_service_account_authz_token().accessToken
     client = build_airavata_client(
-        access_token, claims={'gatewayID': settings.GATEWAY_ID})
+        access_token, claims={"gatewayID": settings.GATEWAY_ID}
+    )
     try:
         yield client.iam
     finally:
@@ -47,17 +47,6 @@ def is_username_available(username):
         return iam.is_username_available(username)
 
 
-def register_user(username, email_address, first_name, last_name, password):
-    with _iam() as iam:
-        return iam.register_user(
-            username, email_address, first_name, last_name, password)
-
-
-def is_user_enabled(username):
-    with _iam() as iam:
-        return iam.is_user_enabled(username)
-
-
 def enable_user(username):
     with _iam() as iam:
         return iam.enable_user(username)
@@ -68,11 +57,6 @@ def delete_user(username):
         return iam.delete_iam_user(username)
 
 
-def is_user_exist(username):
-    with _iam() as iam:
-        return iam.is_user_exist(username)
-
-
 def get_user(username):
     with _iam() as iam:
         return iam.get_iam_user(username)
@@ -83,68 +67,37 @@ def get_users(offset, limit, search=None):
         return iam.get_iam_users(offset, limit, search or "")
 
 
-def reset_user_password(username, new_password):
-    with _iam() as iam:
-        return iam.reset_user_password(username, new_password)
-
-
 def update_username(username, new_username):
     # make sure that new_username is available
     if not is_username_available(new_username):
-        raise Exception(f"Can't change username of {username} to {new_username} because it is not available")
+        raise Exception(
+            f"Can't change username of {username} to {new_username} because it is not available"
+        )
     # fetch user representation
     authz_token = utils.get_service_account_authz_token()
-    headers = {'Authorization': f'Bearer {authz_token.accessToken}'}
+    headers = {"Authorization": f"Bearer {authz_token.accessToken}"}
     parsed = urlparse(settings.KEYCLOAK_AUTHORIZE_URL)
-    r = requests.get(f"{parsed.scheme}://{parsed.netloc}/auth/admin/realms/{settings.GATEWAY_ID}/users",
-                     params={'username': username},
-                     headers=headers)
+    r = requests.get(
+        f"{parsed.scheme}://{parsed.netloc}/auth/admin/realms/{settings.GATEWAY_ID}/users",
+        params={"username": username},
+        headers=headers,
+    )
     r.raise_for_status()
     user_list = r.json()
     user = None
     # The users search finds partial matches. Loop to find the exact match.
     for u in user_list:
-        if u['username'] == username:
+        if u["username"] == username:
             user = u
             break
     if user is None:
         raise Exception(f"Could not find user {username}")
 
     # update username
-    user['username'] = new_username
-    r = requests.put(f"{parsed.scheme}://{parsed.netloc}/auth/admin/realms/{settings.GATEWAY_ID}/users/{user['id']}",
-                     json=user,
-                     headers=headers)
-    r.raise_for_status()
-
-
-def update_user(username, first_name=None, last_name=None, email=None):
-    # fetch user representation
-    authz_token = utils.get_service_account_authz_token()
-    headers = {'Authorization': f'Bearer {authz_token.accessToken}'}
-    parsed = urlparse(settings.KEYCLOAK_AUTHORIZE_URL)
-    r = requests.get(f"{parsed.scheme}://{parsed.netloc}/auth/admin/realms/{settings.GATEWAY_ID}/users",
-                     params={'username': username},
-                     headers=headers)
-    r.raise_for_status()
-    user_list = r.json()
-    user = None
-    # The users search finds partial matches. Loop to find the exact match.
-    for u in user_list:
-        if u['username'] == username:
-            user = u
-            break
-    if user is None:
-        raise Exception(f"Could not find user {username}")
-
-    # update user
-    if first_name is not None:
-        user['firstName'] = first_name
-    if last_name is not None:
-        user['lastName'] = last_name
-    if email is not None:
-        user['email'] = email
-    r = requests.put(f"{parsed.scheme}://{parsed.netloc}/auth/admin/realms/{settings.GATEWAY_ID}/users/{user['id']}",
-                     json=user,
-                     headers=headers)
+    user["username"] = new_username
+    r = requests.put(
+        f"{parsed.scheme}://{parsed.netloc}/auth/admin/realms/{settings.GATEWAY_ID}/users/{user['id']}",
+        json=user,
+        headers=headers,
+    )
     r.raise_for_status()
diff --git a/airavata-django-portal/django_airavata/apps/auth/middleware.py b/airavata-django-portal/django_airavata/apps/auth/middleware.py
index 86ec84fa4..e04018e9a 100644
--- a/airavata-django-portal/django_airavata/apps/auth/middleware.py
+++ b/airavata-django-portal/django_airavata/apps/auth/middleware.py
@@ -1,19 +1,20 @@
 """Django Airavata Auth Middleware."""
+
 import json
 import logging
 import time
 
 from django.conf import settings
-from django.contrib.auth import logout
-from django.contrib.auth.models import AnonymousUser
 
 from . import utils
+from .token_authentication import AnonymousUser
 
 log = logging.getLogger(__name__)
 
 
 def authz_token_middleware(get_response):
     """Automatically add the 'authz_token' to the request."""
+
     def middleware(request):
 
         authz_token = None
@@ -21,8 +22,10 @@ def authz_token_middleware(get_response):
             authz_token = utils.get_authz_token(request)
             # If we can't construct an authz_token then need to re-login
             if authz_token is None:
-                # logout user since no longer logged in with IAM server
-                logout(request)
+                # no longer logged in with the IAM server: clear the session
+                # (cache-backed, no DB) and drop back to anonymous
+                request.session.flush()
+                request.user = AnonymousUser()
 
         request.authz_token = authz_token
 
@@ -34,19 +37,22 @@ def authz_token_middleware(get_response):
 def _gateway_groups_dict(request):
     """Fetch the gateway's admin group ids via the gRPC compute facade."""
     gg = request.airavata.compute.get_gateway_groups()
-    return {'adminsGroupId': gg.admins_group_id,
-            'readOnlyAdminsGroupId': gg.read_only_admins_group_id,
-            'defaultGatewayUsersGroupId': gg.default_gateway_users_group_id}
+    return {
+        "adminsGroupId": gg.admins_group_id,
+        "readOnlyAdminsGroupId": gg.read_only_admins_group_id,
+        "defaultGatewayUsersGroupId": gg.default_gateway_users_group_id,
+    }
 
 
 def set_admin_group_attributes(request, gateway_groups=None):
     """Set is_gateway_admin and is_read_only_gateway_admin request attrs."""
     if gateway_groups is None:
         gateway_groups = _gateway_groups_dict(request)
-    admins_group_id = gateway_groups['adminsGroupId']
-    read_only_admins_group_id = gateway_groups['readOnlyAdminsGroupId']
+    admins_group_id = gateway_groups["adminsGroupId"]
+    read_only_admins_group_id = gateway_groups["readOnlyAdminsGroupId"]
     group_memberships = request.airavata.sharing.gm_get_all_groups_user_belongs(
-        request.user.username + "@" + settings.GATEWAY_ID)
+        request.user.username + "@" + settings.GATEWAY_ID
+    )
     group_ids = [group.id for group in group_memberships]
     request.is_gateway_admin = admins_group_id in group_ids
     request.is_read_only_gateway_admin = read_only_admins_group_id in group_ids
@@ -54,6 +60,7 @@ def set_admin_group_attributes(request, gateway_groups=None):
 
 def gateway_groups_middleware(get_response):
     """Add 'is_gateway_admin' and 'is_read_only_gateway_admin' to request."""
+
     def middleware(request):
 
         request.is_gateway_admin = False
@@ -65,14 +72,19 @@ def gateway_groups_middleware(get_response):
         try:
             # Load the GatewayGroups and check if user is in the Admins and/or
             # Read Only Admins groups
-            if not request.session.get('GATEWAY_GROUPS'):
-                request.session['GATEWAY_GROUPS'] = _gateway_groups_dict(request)
-            set_admin_group_attributes(request, gateway_groups=request.session.get("GATEWAY_GROUPS"))
+            if not request.session.get("GATEWAY_GROUPS"):
+                request.session["GATEWAY_GROUPS"] = _gateway_groups_dict(request)
+            set_admin_group_attributes(
+                request, gateway_groups=request.session.get("GATEWAY_GROUPS")
+            )
         except Exception as e:
-            log.warning("Failed to set is_gateway_admin, "
-                        "is_read_only_gateway_admin for user", exc_info=e)
+            log.warning(
+                "Failed to set is_gateway_admin, is_read_only_gateway_admin for user",
+                exc_info=e,
+            )
 
         return get_response(request)
+
     return middleware
 
 
@@ -81,6 +93,7 @@ def gateway_groups_middleware(get_response):
 # here so the cutover can add them to MIDDLEWARE in one step.
 # ---------------------------------------------------------------------------
 
+
 def request_data_middleware(get_response):
     """Augment every request with ``request.data`` and ``request.query_params``.
 
@@ -90,17 +103,18 @@ def request_data_middleware(get_response):
     ``request.GET``. Idempotent and safe for all paths (parse failures degrade to
     an empty dict).
     """
+
     def middleware(request):
         request.query_params = request.GET
 
-        content_type = (request.META.get('CONTENT_TYPE', '') or '').lower()
-        if 'application/json' in content_type:
+        content_type = (request.META.get("CONTENT_TYPE", "") or "").lower()
+        if "application/json" in content_type:
             try:
                 body = request.body
                 request.data = json.loads(body) if body else {}
             except (ValueError, json.JSONDecodeError):
                 request.data = {}
-        elif request.method in ('POST', 'PUT', 'PATCH'):
+        elif request.method in ("POST", "PUT", "PATCH"):
             # form-encoded / multipart — Django parsed it into request.POST.
             request.data = request.POST.dict()
         else:
@@ -132,42 +146,41 @@ def session_keycloak_user_middleware(get_response):
     def _decode(token):
         signing_key = _jwks().get_signing_key_from_jwt(token)
         return jwt.decode(
-            token, signing_key.key, algorithms=['RS256'],
-            options={'verify_aud': False})
+            token, signing_key.key, algorithms=["RS256"], options={"verify_aud": False}
+        )
 
     def middleware(request):
         # If a prior middleware already authenticated the request, no-op.
-        user = getattr(request, 'user', None)
-        if user is not None and getattr(user, 'is_authenticated', False):
+        user = getattr(request, "user", None)
+        if user is not None and getattr(user, "is_authenticated", False):
             return get_response(request)
 
         # This middleware replaces AuthenticationMiddleware, so it owns
         # request.user. Default to Anonymous; the token paths below upgrade it.
         request.user = AnonymousUser()
 
-        if 'ACCESS_TOKEN' not in request.session:
+        if "ACCESS_TOKEN" not in request.session:
             return get_response(request)
 
         now = time.time()
-        access_token = request.session['ACCESS_TOKEN']
-        access_expires_at = request.session.get('ACCESS_TOKEN_EXPIRES_AT', 0)
-        refresh_expires_at = request.session.get('REFRESH_TOKEN_EXPIRES_AT', 0)
+        access_token = request.session["ACCESS_TOKEN"]
+        access_expires_at = request.session.get("ACCESS_TOKEN_EXPIRES_AT", 0)
+        refresh_expires_at = request.session.get("REFRESH_TOKEN_EXPIRES_AT", 0)
 
         try:
             if access_expires_at > now:
                 claims = _decode(access_token)
                 request.user = KeycloakUser(claims)
-            elif ('REFRESH_TOKEN' in request.session and
-                    refresh_expires_at > now):
+            elif "REFRESH_TOKEN" in request.session and refresh_expires_at > now:
                 token = utils.refresh_access_token(request)
                 if token is None:
                     request.session.flush()
                     return get_response(request)
                 utils.store_token_in_session(request, token)
-                claims = _decode(token['access_token'])
+                claims = _decode(token["access_token"])
                 request.user = KeycloakUser(claims)
             # else: token expired and no usable refresh token; leave Anonymous.
-        except Exception as e:  # noqa: BLE001 - any failure is an auth failure
+        except Exception as e:
             log.warning("Failed to derive user from session token: %s", e)
             request.session.flush()
 
@@ -194,21 +207,24 @@ def keycloak_bearer_middleware(get_response):
     from .token_authentication import KeycloakUser, _jwks
 
     def middleware(request):
-        user = getattr(request, 'user', None)
-        if user is not None and getattr(user, 'is_authenticated', False):
+        user = getattr(request, "user", None)
+        if user is not None and getattr(user, "is_authenticated", False):
             return get_response(request)
 
-        header = request.META.get('HTTP_AUTHORIZATION', '')
-        if not header.startswith('Bearer '):
+        header = request.META.get("HTTP_AUTHORIZATION", "")
+        if not header.startswith("Bearer "):
             return get_response(request)
 
-        token = header[len('Bearer '):].strip()
+        token = header[len("Bearer ") :].strip()
         try:
             signing_key = _jwks().get_signing_key_from_jwt(token)
             claims = jwt.decode(
-                token, signing_key.key, algorithms=['RS256'],
-                options={'verify_aud': False})
-        except Exception as e:  # noqa: BLE001 - any failure is an auth failure
+                token,
+                signing_key.key,
+                algorithms=["RS256"],
+                options={"verify_aud": False},
+            )
+        except Exception as e:
             log.warning("Keycloak bearer token validation failed: %s", e)
             request.user = AnonymousUser()
             return get_response(request)
@@ -216,8 +232,11 @@ def keycloak_bearer_middleware(get_response):
         keycloak_user = KeycloakUser(claims)
         authz_token = utils.AuthzToken(
             accessToken=token,
-            claimsMap={'gatewayID': settings.GATEWAY_ID,
-                       'userName': keycloak_user.username})
+            claimsMap={
+                "gatewayID": settings.GATEWAY_ID,
+                "userName": keycloak_user.username,
+            },
+        )
         request.user = keycloak_user
         request.authz_token = authz_token
         # The session-based gateway_groups_middleware sets these; pure-token auth
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0001_initial.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0001_initial.py
deleted file mode 100644
index 8745b6f94..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0001_initial.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.16 on 2018-10-25 15:32
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import uuid
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='EmailVerification',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('username', models.CharField(max_length=64)),
-                ('verification_code', models.CharField(default=uuid.uuid4, max_length=36, unique=True)),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-                ('verified', models.BooleanField(default=False)),
-            ],
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0002_emailtemplate.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0002_emailtemplate.py
deleted file mode 100644
index b9006b364..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0002_emailtemplate.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.16 on 2018-10-29 18:23
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='EmailTemplate',
-            fields=[
-                ('template_type', models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template')], primary_key=True, serialize=False)),
-                ('subject', models.CharField(max_length=255)),
-                ('body', models.TextField()),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-                ('updated_date', models.DateTimeField(auto_now=True)),
-            ],
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0003_default_email_templates.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0003_default_email_templates.py
deleted file mode 100644
index c21caccae..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0003_default_email_templates.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.16 on 2018-10-29 18:33
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-from django_airavata.apps.auth.models import (
-    NEW_USER_EMAIL_TEMPLATE,
-    VERIFY_EMAIL_TEMPLATE
-)
-
-
-def default_templates(apps, schema_editor):
-
-    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
-    verify_email_template = EmailTemplate(
-        template_type=VERIFY_EMAIL_TEMPLATE,
-        subject="{{first_name}} {{last_name}} ({{username}}), "
-                "Please Verify Your Email Account in {{portal_title}}",
-        body="""
-        <p>
-        Dear {{first_name}} {{last_name}},
-        </p>
-
-        <p>
-        Someone has created an account with this email address. If this was
-        you, click the link below to verify your email address:
-        </p>
-
-        <p><a href="{{url}}">{{url}}</a></p>
-
-        <p>If you didn't create this account, just ignore this message.</p>
-        """.strip())
-    verify_email_template.save()
-    new_user_email_template = EmailTemplate(
-        template_type=NEW_USER_EMAIL_TEMPLATE,
-        subject="New User Account Was Created Successfully",
-        body="""
-        <p>Gateway Portal: {{http_host}}</p>
-        <p>Tenant: {{gateway_id}}</p>
-        <p>Username: {{username}}</p>
-        <p>Name: {{first_name}} {{last_name}}</p>
-        <p>Email: {{email}}</p>
-        """.strip()
-    )
-    new_user_email_template.save()
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0002_emailtemplate'),
-    ]
-
-    operations = [
-        migrations.RunPython(default_templates)
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0004_password_reset_request.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0004_password_reset_request.py
deleted file mode 100644
index e8fed01ea..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0004_password_reset_request.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.20 on 2019-05-07 15:49
-from __future__ import unicode_literals
-
-import uuid
-
-from django.db import migrations, models
-
-from django_airavata.apps.auth.models import PASSWORD_RESET_EMAIL_TEMPLATE
-
-
-def default_templates(apps, schema_editor):
-
-    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
-    verify_email_template = EmailTemplate(
-        template_type=PASSWORD_RESET_EMAIL_TEMPLATE,
-        subject="{{first_name}} {{last_name}} ({{username}}), "
-                "Reset your password in {{portal_title}}",
-        body="""
-        <p>
-        Dear {{first_name}} {{last_name}},
-        </p>
-
-        <p>
-        Please click the link below to reset your password. This link is
-        valid for 24 hours.
-        </p>
-
-        <p><a href="{{url}}">{{url}}</a></p>
-
-        <p>If you didn't request to reset your password, just ignore this message.</p>
-        """.strip())
-    verify_email_template.save()
-
-
-def delete_default_templates(apps, schema_editor):
-    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
-    EmailTemplate.objects.filter(
-        template_type=PASSWORD_RESET_EMAIL_TEMPLATE).delete()
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0003_default_email_templates'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='PasswordResetRequest',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('username', models.CharField(max_length=64)),
-                ('reset_code', models.CharField(default=uuid.uuid4, max_length=36, unique=True)),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-            ],
-        ),
-        migrations.AlterField(
-            model_name='emailtemplate',
-            name='template_type',
-            field=models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template')], primary_key=True, serialize=False),
-        ),
-        migrations.RunPython(default_templates, reverse_code=delete_default_templates)
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
deleted file mode 100644
index eeb4b990b..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.21 on 2019-12-11 20:11
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-from django_airavata.apps.auth.models import USER_ADDED_TO_GROUP_TEMPLATE
-
-
-def default_templates(apps, schema_editor):
-
-    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
-    user_added_to_group_template = EmailTemplate(
-        template_type=USER_ADDED_TO_GROUP_TEMPLATE,
-        subject="You've been added to group{{ group_names|length|pluralize }} [{{group_names|join:'] and ['}}] in {{portal_title}}",
-        body="""
-        <p>
-        Dear {{first_name}} {{last_name}},
-        </p>
-
-        <p>
-        Your user account (username {{username}}) has been added to the
-        group{{ group_names|length|pluralize }} {{group_names|join:' and '}}.
-        {{portal_title}} uses groups to share applications and experiments.
-        </p>
-
-        <p>
-        You may have access to additional applications now that you are a
-        member of {{group_names|join:' and '}}. To check what applications you
-        have access to, please check: <a href="{{dashboard_url}}">{{dashboard_url}}</a>.
-        </p>
-
-        <p>
-        You may also have access to additional experiments. To check what
-        experiments you have access to, please check: <a
-        href="{{experiments_url}}">{{experiments_url}}</a>.
-        </p>
-
-        <p>
-        Please let us know if you have any questions.  Thanks.
-        </p>
-        """.strip())
-    user_added_to_group_template.save()
-
-
-def delete_default_templates(apps, schema_editor):
-    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
-    EmailTemplate.objects.filter(
-        template_type=USER_ADDED_TO_GROUP_TEMPLATE).delete()
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0004_password_reset_request'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='emailtemplate',
-            name='template_type',
-            field=models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template'), (4, 'User Added to Group Template')], primary_key=True, serialize=False),
-        ),
-        migrations.RunPython(default_templates,
-                             reverse_code=delete_default_templates)
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0006_emailverification_next.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0006_emailverification_next.py
deleted file mode 100644
index b68cd5c06..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0006_emailverification_next.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.28 on 2020-02-26 21:13
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0005_auto_20191211_2011'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='emailverification',
-            name='next',
-            field=models.CharField(blank=True, max_length=255),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0007_auto_20200917_1610.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0007_auto_20200917_1610.py
deleted file mode 100644
index 4938ebf1c..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0007_auto_20200917_1610.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2020-09-17 16:10
-from __future__ import unicode_literals
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('django_airavata_auth', '0006_emailverification_next'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='UserInfo',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('claim', models.CharField(max_length=64)),
-                ('value', models.CharField(max_length=255)),
-            ],
-        ),
-        migrations.CreateModel(
-            name='UserProfile',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('username_locked', models.BooleanField(default=False)),
-                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_profile', to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-        migrations.AddField(
-            model_name='userinfo',
-            name='user_profile',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_airavata_auth.UserProfile'),
-        ),
-        migrations.AlterUniqueTogether(
-            name='userinfo',
-            unique_together=set([('user_profile', 'claim')]),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py
deleted file mode 100644
index 9a327b067..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Generated by Django 2.2.17 on 2021-04-22 18:38
-
-import uuid
-
-import django.db.models.deletion
-from django.conf import settings
-from django.db import migrations, models
-
-from django_airavata.apps.auth.models import VERIFY_EMAIL_CHANGE_TEMPLATE
-
-
-def default_templates(apps, schema_editor):
-    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
-    verify_email_template = EmailTemplate(
-        template_type=VERIFY_EMAIL_CHANGE_TEMPLATE,
-        subject="{{first_name}} {{last_name}} ({{username}}), "
-                "Please Verify Your New Email Address in {{portal_title}}",
-        body="""
-        <p>
-        Dear {{first_name}} {{last_name}},
-        </p>
-
-        <p>
-        Before your email address change can be processed, you need to verify
-        your new email address ({{email}}). Click the link below to verify your email
-        address:
-        </p>
-
-        <p><a href="{{url}}">{{url}}</a></p>
-        """.strip())
-    verify_email_template.save()
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('django_airavata_auth', '0007_auto_20200917_1610'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='emailtemplate',
-            name='template_type',
-            field=models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template'), (4, 'User Added to Group Template'), (5, 'Verify Email Change Template')], primary_key=True, serialize=False),
-        ),
-        migrations.CreateModel(
-            name='PendingEmailChange',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('email_address', models.EmailField(max_length=254)),
-                ('verification_code', models.CharField(default=uuid.uuid4, max_length=36, unique=True)),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-                ('verified', models.BooleanField(default=False)),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-        migrations.RunPython(default_templates, migrations.RunPython.noop),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0009_alter_emailverification_next.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0009_alter_emailverification_next.py
deleted file mode 100644
index 0b554e949..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0009_alter_emailverification_next.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.2.8 on 2021-11-05 21:22
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0008_auto_20210422_1838'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='emailverification',
-            name='next',
-            field=models.CharField(max_length=255, null=True),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0009_auto_20210625_1725.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0009_auto_20210625_1725.py
deleted file mode 100644
index 1d381999c..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0009_auto_20210625_1725.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Generated by Django 2.2.23 on 2021-06-25 17:25
-
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0008_auto_20210422_1838'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='userinfo',
-            name='created_date',
-            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
-            preserve_default=False,
-        ),
-        migrations.AddField(
-            model_name='userinfo',
-            name='updated_date',
-            field=models.DateTimeField(auto_now=True),
-        ),
-        migrations.CreateModel(
-            name='IDPUserInfo',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('idp_alias', models.CharField(max_length=64)),
-                ('claim', models.CharField(max_length=64)),
-                ('value', models.CharField(max_length=255)),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-                ('updated_date', models.DateTimeField(auto_now=True)),
-                ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='idp_userinfo', to='django_airavata_auth.UserProfile')),
-            ],
-            options={
-                'unique_together': {('user_profile', 'claim', 'idp_alias')},
-            },
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0010_userprofile_username_initialized.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0010_userprofile_username_initialized.py
deleted file mode 100644
index 4b4745921..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0010_userprofile_username_initialized.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.2.8 on 2021-10-12 20:35
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0009_auto_20210625_1725'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='userprofile',
-            name='username_initialized',
-            field=models.BooleanField(default=False),
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0011_remove_userprofile_username_locked.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0011_remove_userprofile_username_locked.py
deleted file mode 100644
index ca95605a7..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0011_remove_userprofile_username_locked.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 3.2.8 on 2021-12-07 16:40
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0010_userprofile_username_initialized'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='userprofile',
-            name='username_locked',
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0012_merge_20211210_2041.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0012_merge_20211210_2041.py
deleted file mode 100644
index 042a9a054..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0012_merge_20211210_2041.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Generated by Django 3.2.8 on 2021-12-10 20:41
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0009_alter_emailverification_next'),
-        ('django_airavata_auth', '0011_remove_userprofile_username_locked'),
-    ]
-
-    operations = [
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0013_auto_20220118_1650.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0013_auto_20220118_1650.py
deleted file mode 100644
index fe52ef290..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0013_auto_20220118_1650.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 3.2.10 on 2022-01-18 16:50
-
-from django.db import migrations
-
-
-def default_username_initialized(apps, schema_editor):
-    UserProfile = apps.get_model("django_airavata_auth", "UserProfile")
-    # Update all existing user's user_profiles to have username_initialized=True
-    UserProfile.objects.all().update(username_initialized=True)
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0012_merge_20211210_2041'),
-    ]
-
-    operations = [
-        migrations.RunPython(default_username_initialized, migrations.RunPython.noop)
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0014_auto_20220217_2255.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0014_auto_20220217_2255.py
deleted file mode 100644
index ff03168f9..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0014_auto_20220217_2255.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 3.2.10 on 2022-02-17 22:55
-
-from django.db import migrations
-
-
-def reset_username_initialized_for_cilogon_usernames(apps, schema_editor):
-    UserProfile = apps.get_model("django_airavata_auth", "UserProfile")
-    # Update all existing user's user_profiles to have
-    # username_initialized=False when the username is invalid because it is a
-    # CILogon sub claim
-    UserProfile.objects.filter(
-        username_initialized=True, user__username__startswith="http://cilogon"
-    ).update(username_initialized=False)
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0013_auto_20220118_1650'),
-    ]
-
-    operations = [
-        migrations.RunPython(reset_username_initialized_for_cilogon_usernames, migrations.RunPython.noop)
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
deleted file mode 100644
index 2b5601cb2..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# Generated by Django 3.2.11 on 2022-03-11 14:25
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0013_auto_20220118_1650'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='ExtendedUserProfileField',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=64)),
-                ('help_text', models.TextField(blank=True)),
-                ('order', models.IntegerField()),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-                ('updated_date', models.DateTimeField(auto_now=True)),
-                ('deleted', models.BooleanField(default=False)),
-            ],
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileAgreementField',
-            fields=[
-                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='user_agreement', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
-                ('checkbox_label', models.TextField(blank=True)),
-            ],
-            bases=('django_airavata_auth.extendeduserprofilefield',),
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileMultiChoiceField',
-            fields=[
-                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='multi_choice', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
-                ('other', models.BooleanField(default=False)),
-            ],
-            bases=('django_airavata_auth.extendeduserprofilefield',),
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileSingleChoiceField',
-            fields=[
-                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='single_choice', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
-                ('other', models.BooleanField(default=False)),
-            ],
-            bases=('django_airavata_auth.extendeduserprofilefield',),
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileTextField',
-            fields=[
-                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='text', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
-            ],
-            bases=('django_airavata_auth.extendeduserprofilefield',),
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileInfo',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('id_value', models.BigIntegerField(null=True)),
-                ('text_value', models.CharField(blank=True, max_length=255)),
-                ('created_date', models.DateTimeField(auto_now_add=True)),
-                ('updated_date', models.DateTimeField(auto_now=True)),
-                ('ext_user_profile_field', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_airavata_auth.extendeduserprofilefield')),
-                ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extended_profile', to='django_airavata_auth.userprofile')),
-            ],
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileFieldLink',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('label', models.TextField()),
-                ('url', models.URLField()),
-                ('order', models.IntegerField()),
-                ('display_link', models.BooleanField(default=True)),
-                ('display_inline', models.BooleanField(default=False)),
-                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='django_airavata_auth.extendeduserprofilefield')),
-            ],
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileSingleChoiceFieldChoice',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('display_text', models.CharField(max_length=255)),
-                ('order', models.IntegerField()),
-                ('deleted', models.BooleanField(default=False)),
-                ('single_choice_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilesinglechoicefield')),
-            ],
-            options={
-                'abstract': False,
-            },
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileMultiChoiceFieldChoice',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('display_text', models.CharField(max_length=255)),
-                ('order', models.IntegerField()),
-                ('deleted', models.BooleanField(default=False)),
-                ('multi_choice_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilemultichoicefield')),
-            ],
-            options={
-                'abstract': False,
-            },
-        ),
-    ]
diff --git a/airavata-django-portal/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py b/airavata-django-portal/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
deleted file mode 100644
index 49da5c183..000000000
--- a/airavata-django-portal/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Generated by Django 3.2.11 on 2022-03-29 17:08
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('django_airavata_auth', '0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='ExtendedUserProfileMultiChoiceValueChoice',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('value', models.BigIntegerField()),
-            ],
-        ),
-        migrations.CreateModel(
-            name='ExtendedUserProfileValue',
... 6342 lines suppressed ...