[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

@ -474,20 +474,15 @@ def packages(request):
if request.method == "GET":
context = get_base_context(request)
context["title"] = "enviPath - Packages"
context["object_type"] = "package"
context["meta"]["current_package"] = context["meta"]["user"].default_package
context["meta"]["can_edit"] = True
reviewed_package_qs = Package.objects.filter(reviewed=True).order_by("created")
unreviewed_package_qs = PackageManager.get_all_readable_packages(current_user).order_by(
"name"
)
# Context for paginated template
context["entity_type"] = "package"
context["api_endpoint"] = "/api/v1/packages/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "packages"
context["reviewed_objects"] = reviewed_package_qs
context["unreviewed_objects"] = unreviewed_package_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/packages_paginated.html", context)
elif request.method == "POST":
if hidden := request.POST.get("hidden", None):
@ -533,29 +528,16 @@ def compounds(request):
if request.method == "GET":
context = get_base_context(request)
context["title"] = "enviPath - Compounds"
context["object_type"] = "compound"
context["meta"]["current_package"] = context["meta"]["user"].default_package
reviewed_compound_qs = Compound.objects.none()
# Context for paginated template
context["entity_type"] = "compound"
context["api_endpoint"] = "/api/v1/compounds/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_mode"] = "tabbed"
context["list_title"] = "compounds"
for p in PackageManager.get_reviewed_packages():
reviewed_compound_qs |= Compound.objects.filter(package=p)
reviewed_compound_qs = reviewed_compound_qs.order_by("name")
if request.GET.get("all"):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": True}
for pw in reviewed_compound_qs
]
}
)
context["reviewed_objects"] = reviewed_compound_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/compounds_paginated.html", context)
elif request.method == "POST":
# delegate to default package
@ -571,32 +553,19 @@ def rules(request):
if request.method == "GET":
context = get_base_context(request)
context["title"] = "enviPath - Rules"
context["object_type"] = "rule"
context["meta"]["current_package"] = context["meta"]["user"].default_package
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Rule": s.SERVER_URL + "/rule"},
]
reviewed_rule_qs = Rule.objects.none()
for p in PackageManager.get_reviewed_packages():
reviewed_rule_qs |= Rule.objects.filter(package=p)
# Context for paginated template
context["entity_type"] = "rule"
context["api_endpoint"] = "/api/v1/rules/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "rules"
reviewed_rule_qs = reviewed_rule_qs.order_by("name")
if request.GET.get("all"):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": True}
for pw in reviewed_rule_qs
]
}
)
context["reviewed_objects"] = reviewed_rule_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/rules_paginated.html", context)
elif request.method == "POST":
# delegate to default package
@ -612,32 +581,19 @@ def reactions(request):
if request.method == "GET":
context = get_base_context(request)
context["title"] = "enviPath - Reactions"
context["object_type"] = "reaction"
context["meta"]["current_package"] = context["meta"]["user"].default_package
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Reaction": s.SERVER_URL + "/reaction"},
]
reviewed_reaction_qs = Reaction.objects.none()
for p in PackageManager.get_reviewed_packages():
reviewed_reaction_qs |= Reaction.objects.filter(package=p).order_by("name")
# Context for paginated template
context["entity_type"] = "reaction"
context["api_endpoint"] = "/api/v1/reactions/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "reactions"
reviewed_reaction_qs = reviewed_reaction_qs.order_by("name")
if request.GET.get("all"):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": True}
for pw in reviewed_reaction_qs
]
}
)
context["reviewed_objects"] = reviewed_reaction_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/reactions_paginated.html", context)
elif request.method == "POST":
# delegate to default package
@ -653,33 +609,19 @@ def pathways(request):
if request.method == "GET":
context = get_base_context(request)
context["title"] = "enviPath - Pathways"
context["object_type"] = "pathway"
context["meta"]["current_package"] = context["meta"]["user"].default_package
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Pathway": s.SERVER_URL + "/pathway"},
]
reviewed_pathway_qs = Pathway.objects.none()
# Context for paginated template
context["entity_type"] = "pathway"
context["api_endpoint"] = "/api/v1/pathways/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "pathways"
for p in PackageManager.get_reviewed_packages():
reviewed_pathway_qs |= Pathway.objects.filter(package=p).order_by("name")
reviewed_pathway_qs = reviewed_pathway_qs.order_by("name")
if request.GET.get("all"):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": True}
for pw in reviewed_pathway_qs
]
}
)
context["reviewed_objects"] = reviewed_pathway_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/pathways_paginated.html", context)
elif request.method == "POST":
# delegate to default package
@ -703,25 +645,13 @@ def scenarios(request):
{"Scenario": s.SERVER_URL + "/scenario"},
]
reviewed_scenario_qs = Scenario.objects.none()
# Context for paginated template
context["entity_type"] = "scenario"
context["api_endpoint"] = "/api/v1/scenarios/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "scenarios"
for p in PackageManager.get_reviewed_packages():
reviewed_scenario_qs |= Scenario.objects.filter(package=p).order_by("name")
reviewed_scenario_qs = reviewed_scenario_qs.order_by("name")
if request.GET.get("all"):
return JsonResponse(
{
"objects": [
{"name": s.name, "url": s.url, "reviewed": True}
for s in reviewed_scenario_qs
]
}
)
context["reviewed_objects"] = reviewed_scenario_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/scenarios_paginated.html", context)
elif request.method == "POST":
# delegate to default package
@ -736,42 +666,28 @@ def models(request):
if request.method == "GET":
context = get_base_context(request)
context["title"] = "enviPath - Models"
context["object_type"] = "model"
context["meta"]["current_package"] = context["meta"]["user"].default_package
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Model": s.SERVER_URL + "/model"},
]
# Keep model_types for potential modal/action use
context["model_types"] = {
"ML Relative Reasoning": "ml-relative-reasoning",
"Rule Based Relative Reasoning": "rule-based-relative-reasoning",
"EnviFormer": "enviformer",
}
for k, v in s.CLASSIFIER_PLUGINS.items():
context["model_types"][v.display()] = k
reviewed_model_qs = EPModel.objects.none()
# Context for paginated template
context["entity_type"] = "model"
context["api_endpoint"] = "/api/v1/models/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models"
for p in PackageManager.get_reviewed_packages():
reviewed_model_qs |= EPModel.objects.filter(package=p).order_by("name")
reviewed_model_qs = reviewed_model_qs.order_by("name")
if request.GET.get("all"):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": True}
for pw in reviewed_model_qs
]
}
)
context["reviewed_objects"] = reviewed_model_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/models_paginated.html", context)
elif request.method == "POST":
current_user = _anonymous_or_real(request)
@ -848,6 +764,10 @@ def package_models(request, package_uuid):
context["meta"]["current_package"] = current_package
context["object_type"] = "model"
context["breadcrumbs"] = breadcrumbs(current_package, "model")
context["entity_type"] = "model"
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/model/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models"
reviewed_model_qs = EPModel.objects.none()
unreviewed_model_qs = EPModel.objects.none()
@ -869,9 +789,6 @@ def package_models(request, package_uuid):
}
)
context["reviewed_objects"] = reviewed_model_qs
context["unreviewed_objects"] = unreviewed_model_qs
context["model_types"] = {
"ML Relative Reasoning": "mlrr",
"Rule Based Relative Reasoning": "rbrr",
@ -884,7 +801,7 @@ def package_models(request, package_uuid):
for k, v in s.CLASSIFIER_PLUGINS.items():
context["model_types"][v.display()] = k
return render(request, "collections/objects_list.html", context)
return render(request, "collections/models_paginated.html", context)
elif request.method == "POST":
log_post_params(request)
@ -1242,6 +1159,11 @@ def package_compounds(request, package_uuid):
context["meta"]["current_package"] = current_package
context["object_type"] = "compound"
context["breadcrumbs"] = breadcrumbs(current_package, "compound")
context["entity_type"] = "compound"
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/compound/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_mode"] = "tabbed"
context["list_title"] = "compounds"
reviewed_compound_qs = Compound.objects.none()
unreviewed_compound_qs = Compound.objects.none()
@ -1267,10 +1189,7 @@ def package_compounds(request, package_uuid):
}
)
context["reviewed_objects"] = reviewed_compound_qs
context["unreviewed_objects"] = unreviewed_compound_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/compounds_paginated.html", context)
elif request.method == "POST":
compound_name = request.POST.get("compound-name")
@ -1389,19 +1308,17 @@ def package_compound_structures(request, package_uuid, compound_uuid):
context["breadcrumbs"] = breadcrumbs(
current_package, "compound", current_compound, "structure"
)
context["entity_type"] = "structure"
context["page_title"] = f"{current_compound.name} - Structures"
context["api_endpoint"] = (
f"/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/"
)
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["compound"] = current_compound
context["list_mode"] = "combined"
context["list_title"] = "structures"
reviewed_compound_structure_qs = CompoundStructure.objects.none()
unreviewed_compound_structure_qs = CompoundStructure.objects.none()
if current_package.reviewed:
reviewed_compound_structure_qs = current_compound.structures.order_by("name")
else:
unreviewed_compound_structure_qs = current_compound.structures.order_by("name")
context["reviewed_objects"] = reviewed_compound_structure_qs
context["unreviewed_objects"] = unreviewed_compound_structure_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/structures_paginated.html", context)
elif request.method == "POST":
structure_name = request.POST.get("structure-name")
@ -1548,6 +1465,10 @@ def package_rules(request, package_uuid):
context["meta"]["current_package"] = current_package
context["object_type"] = "rule"
context["breadcrumbs"] = breadcrumbs(current_package, "rule")
context["entity_type"] = "rule"
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/rule/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "rules"
reviewed_rule_qs = Rule.objects.none()
unreviewed_rule_qs = Rule.objects.none()
@ -1569,10 +1490,7 @@ def package_rules(request, package_uuid):
}
)
context["reviewed_objects"] = reviewed_rule_qs
context["unreviewed_objects"] = unreviewed_rule_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/rules_paginated.html", context)
elif request.method == "POST":
log_post_params(request)
@ -1750,11 +1668,15 @@ def package_reactions(request, package_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_package.name} - Reactions"
context["title"] = f"enviPath - {current_package.name} - Reactions"
context["meta"]["current_package"] = current_package
context["object_type"] = "reaction"
context["breadcrumbs"] = breadcrumbs(current_package, "reaction")
context["entity_type"] = "reaction"
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/reaction/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "reactions"
reviewed_reaction_qs = Reaction.objects.none()
unreviewed_reaction_qs = Reaction.objects.none()
@ -1780,10 +1702,7 @@ def package_reactions(request, package_uuid):
}
)
context["reviewed_objects"] = reviewed_reaction_qs
context["unreviewed_objects"] = unreviewed_reaction_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/reactions_paginated.html", context)
elif request.method == "POST":
reaction_name = request.POST.get("reaction-name")
@ -1902,6 +1821,10 @@ def package_pathways(request, package_uuid):
context["meta"]["current_package"] = current_package
context["object_type"] = "pathway"
context["breadcrumbs"] = breadcrumbs(current_package, "pathway")
context["entity_type"] = "pathway"
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/pathway/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "pathways"
reviewed_pathway_qs = Pathway.objects.none()
unreviewed_pathway_qs = Pathway.objects.none()
@ -1925,10 +1848,7 @@ def package_pathways(request, package_uuid):
}
)
context["reviewed_objects"] = reviewed_pathway_qs
context["unreviewed_objects"] = unreviewed_pathway_qs
return render(request, "collections/objects_list.html", context)
return render(request, "collections/pathways_paginated.html", context)
elif request.method == "POST":
log_post_params(request)
@ -2465,6 +2385,10 @@ def package_scenarios(request, package_uuid):
context["meta"]["current_package"] = current_package
context["object_type"] = "scenario"
context["breadcrumbs"] = breadcrumbs(current_package, "scenario")
context["entity_type"] = "scenario"
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/scenario/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "scenarios"
reviewed_scenario_qs = Scenario.objects.none()
unreviewed_scenario_qs = Scenario.objects.none()
@ -2490,9 +2414,6 @@ def package_scenarios(request, package_uuid):
}
)
context["reviewed_objects"] = reviewed_scenario_qs
context["unreviewed_objects"] = unreviewed_scenario_qs
from envipy_additional_information import (
SEDIMENT_ADDITIONAL_INFORMATION,
SLUDGE_ADDITIONAL_INFORMATION,
@ -2527,7 +2448,7 @@ def package_scenarios(request, package_uuid):
context["soil_additional_information"] = SOIL_ADDITIONAL_INFORMATION
context["sediment_additional_information"] = SEDIMENT_ADDITIONAL_INFORMATION
return render(request, "collections/objects_list.html", context)
return render(request, "collections/scenarios_paginated.html", context)
elif request.method == "POST":
log_post_params(request)