[Feature] Server pagination implementation (#243)

## Major Changes
- Implement a REST style API app in epapi
- Currently implements a GET method for all entity types in the browse menu (both package level and global)
- Provides paginated results per default with query style filtering for reviewed vs unreviewed.
- Provides new paginated templates with thin wrappers per entity types for easier maintainability
- Implements e2e tests for the API

## Minor changes
- Added more comprehensive gitignore to cover coverage reports and other test/node.js etc. data.
- Add additional CI file for API tests that only gets triggered on API relevant changes.

## ⚠️ Currently only works with session-based authentication. Token based will be added in new PR.

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Co-authored-by: jebus <lorsbach@envipath.com>
Reviewed-on: enviPath/enviPy#243
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
This commit is contained in:
2025-12-15 11:34:53 +13:00
committed by jebus
parent d2d475b990
commit 8adb93012a
59 changed files with 3101 additions and 620 deletions

View File

View File

@ -0,0 +1,41 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from uuid import UUID
from epdb.models import Compound
from ..pagination import EnhancedPageNumberPagination
from ..schemas import CompoundOutSchema, ReviewStatusFilter
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
router = Router()
@router.get("/compounds/", response=EnhancedPageNumberPagination.Output[CompoundOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_all_compounds(request):
"""
List all compounds from reviewed packages.
"""
return get_user_entities_qs(Compound, request.user).order_by("name").all()
@router.get(
"/package/{uuid:package_uuid}/compound/",
response=EnhancedPageNumberPagination.Output[CompoundOutSchema],
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_package_compounds(request, package_uuid: UUID):
"""
List all compounds for a specific package.
"""
user = request.user
return get_package_scoped_entities_qs(Compound, package_uuid, user).order_by("name").all()

View File

@ -0,0 +1,41 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from uuid import UUID
from epdb.models import EPModel
from ..pagination import EnhancedPageNumberPagination
from ..schemas import ModelOutSchema, ReviewStatusFilter
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
router = Router()
@router.get("/models/", response=EnhancedPageNumberPagination.Output[ModelOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_all_models(request):
"""
List all models from reviewed packages.
"""
return get_user_entities_qs(EPModel, request.user).order_by("name").all()
@router.get(
"/package/{uuid:package_uuid}/model/",
response=EnhancedPageNumberPagination.Output[ModelOutSchema],
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_package_models(request, package_uuid: UUID):
"""
List all models for a specific package.
"""
user = request.user
return get_package_scoped_entities_qs(EPModel, package_uuid, user).order_by("name").all()

View File

@ -0,0 +1,27 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
import logging
from ..dal import get_user_packages_qs
from ..pagination import EnhancedPageNumberPagination
from ..schemas import PackageOutSchema, SelfReviewStatusFilter
router = Router()
logger = logging.getLogger(__name__)
@router.get("/packages/", response=EnhancedPageNumberPagination.Output[PackageOutSchema], auth=None)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=SelfReviewStatusFilter,
)
def list_all_packages(request):
"""
List packages accessible to the user.
"""
user = request.user
qs = get_user_packages_qs(user)
return qs.order_by("name").all()

View File

@ -0,0 +1,42 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from uuid import UUID
from epdb.models import Pathway
from ..pagination import EnhancedPageNumberPagination
from ..schemas import PathwayOutSchema, ReviewStatusFilter
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
router = Router()
@router.get("/pathways/", response=EnhancedPageNumberPagination.Output[PathwayOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_all_pathways(request):
"""
List all pathways from reviewed packages.
"""
user = request.user
return get_user_entities_qs(Pathway, user).order_by("name").all()
@router.get(
"/package/{uuid:package_uuid}/pathway/",
response=EnhancedPageNumberPagination.Output[PathwayOutSchema],
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_package_pathways(request, package_uuid: UUID):
"""
List all pathways for a specific package.
"""
user = request.user
return get_package_scoped_entities_qs(Pathway, package_uuid, user).order_by("name").all()

View File

@ -0,0 +1,42 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from uuid import UUID
from epdb.models import Reaction
from ..pagination import EnhancedPageNumberPagination
from ..schemas import ReactionOutSchema, ReviewStatusFilter
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
router = Router()
@router.get("/reactions/", response=EnhancedPageNumberPagination.Output[ReactionOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_all_reactions(request):
"""
List all reactions from reviewed packages.
"""
user = request.user
return get_user_entities_qs(Reaction, user).order_by("name").all()
@router.get(
"/package/{uuid:package_uuid}/reaction/",
response=EnhancedPageNumberPagination.Output[ReactionOutSchema],
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_package_reactions(request, package_uuid: UUID):
"""
List all reactions for a specific package.
"""
user = request.user
return get_package_scoped_entities_qs(Reaction, package_uuid, user).order_by("name").all()

View File

@ -0,0 +1,42 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from uuid import UUID
from epdb.models import Rule
from ..pagination import EnhancedPageNumberPagination
from ..schemas import ReviewStatusFilter, RuleOutSchema
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
router = Router()
@router.get("/rules/", response=EnhancedPageNumberPagination.Output[RuleOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_all_rules(request):
"""
List all rules from reviewed packages.
"""
user = request.user
return get_user_entities_qs(Rule, user).order_by("name").all()
@router.get(
"/package/{uuid:package_uuid}/rule/",
response=EnhancedPageNumberPagination.Output[RuleOutSchema],
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_package_rules(request, package_uuid: UUID):
"""
List all rules for a specific package.
"""
user = request.user
return get_package_scoped_entities_qs(Rule, package_uuid, user).order_by("name").all()

View File

@ -0,0 +1,36 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from uuid import UUID
from epdb.models import Scenario
from ..pagination import EnhancedPageNumberPagination
from ..schemas import ReviewStatusFilter, ScenarioOutSchema
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
router = Router()
@router.get("/scenarios/", response=EnhancedPageNumberPagination.Output[ScenarioOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_all_scenarios(request):
user = request.user
return get_user_entities_qs(Scenario, user).order_by("name").all()
@router.get(
"/package/{uuid:package_uuid}/scenario/",
response=EnhancedPageNumberPagination.Output[ScenarioOutSchema],
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=ReviewStatusFilter,
)
def list_package_scenarios(request, package_uuid: UUID):
user = request.user
return get_package_scoped_entities_qs(Scenario, package_uuid, user).order_by("name").all()

View File

@ -0,0 +1,50 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from uuid import UUID
from ..pagination import EnhancedPageNumberPagination
from ..schemas import CompoundStructureOutSchema, StructureReviewStatusFilter
from ..dal import (
get_user_structures_qs,
get_package_compound_scoped_structure_qs,
)
router = Router()
@router.get(
"/structures/", response=EnhancedPageNumberPagination.Output[CompoundStructureOutSchema]
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=StructureReviewStatusFilter,
)
def list_all_structures(request):
"""
List all structures from all packages.
"""
user = request.user
return get_user_structures_qs(user).order_by("name").all()
@router.get(
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/",
response=EnhancedPageNumberPagination.Output[CompoundStructureOutSchema],
)
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
filter_schema=StructureReviewStatusFilter,
)
def list_package_structures(request, package_uuid: UUID, compound_uuid: UUID):
"""
List all structures for a specific package and compound.
"""
user = request.user
return (
get_package_compound_scoped_structure_qs(package_uuid, compound_uuid, user)
.order_by("name")
.all()
)