Skip to content

Commit 12d3bb9

Browse files
authored
Merge pull request #1894 from OpenEnergyPlatform/feature-1890-http-api-schenario-bundle-scenario-dataset
Feature-1890-http-api-schenario-bundle-scenario-dataset
2 parents 0604d86 + 32f4bf7 commit 12d3bb9

File tree

18 files changed

+618
-47
lines changed

18 files changed

+618
-47
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ venv*/
8686
apache*
8787
/oep-django-5
8888

89-
9089
.DS_Store
9190

9291
# Deployment files

api/serializers.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
from re import match
2+
from uuid import UUID
3+
14
from django.urls import reverse
25
from rest_framework import serializers
36

7+
from dataedit.helper import get_readable_table_name
48
from dataedit.models import Table
59
from modelview.models import Energyframework, Energymodel
10+
from oeplatform.settings import URL
611

712

813
class EnergyframeworkSerializer(serializers.ModelSerializer):
@@ -53,3 +58,104 @@ class Meta:
5358
model = Table
5459
# fields = ["id", "model_name", "acronym", "url"]
5560
fields = ["id", "name", "human_readable_name", "url"]
61+
62+
63+
class DatasetSerializer(serializers.Serializer):
64+
name = serializers.CharField(max_length=255, required=True)
65+
external_url = serializers.URLField(
66+
max_length=1000, required=False, allow_null=True
67+
)
68+
type = serializers.ChoiceField(choices=["input", "output"], required=True)
69+
# title = serializers.SerializerMethodField()
70+
71+
# ✅ Basic validation for 'name' (regex check only)
72+
def validate_name(self, value):
73+
if not match(r"^[\w]+$", value):
74+
raise serializers.ValidationError(
75+
"Dataset name should contain only alphanumeric characters "
76+
"and underscores."
77+
)
78+
return value # Don't check DB here, do it in validate()
79+
80+
# ✅ Main validation logic (includes db check for object existence)
81+
def validate(self, data):
82+
name = data.get("name")
83+
external_url = data.get("external_url")
84+
85+
if external_url:
86+
# ✅ External URL provided → Skip DB check for 'name'
87+
if not external_url.startswith("https://databus.openenergyplatform.org"):
88+
raise serializers.ValidationError(
89+
{
90+
"external_url": (
91+
"If you want to link distributions stored outside the OEP, "
92+
"please use the Databus: "
93+
"https://databus.openenergyplatform.org/app/publish-wizard "
94+
"to register your data and use the file or version URI as "
95+
"a persistent identifier."
96+
)
97+
}
98+
)
99+
data["name"] = f"{name} (external dataset)"
100+
else:
101+
# ✅ No external URL → Validate 'name' in the database
102+
if not Table.objects.filter(name=name).exists():
103+
raise serializers.ValidationError(
104+
{
105+
"name": f"Dataset '{name}' does not exist in the database."
106+
"If you want to add links to external distributions please "
107+
"add 'external_url' to the request body."
108+
}
109+
)
110+
full_label = self.get_title(data)
111+
if full_label:
112+
data["name"] = full_label
113+
114+
# ✅ Generate internal distribution URL
115+
reversed_url = reverse(
116+
"dataedit:view",
117+
kwargs={"schema": "scenario", "table": name},
118+
)
119+
data["external_url"] = f"{URL}{reversed_url}"
120+
121+
return data # Return updated data with 'distribution_url' if applicable
122+
123+
def get_title(self, data):
124+
name = data.get("name")
125+
# ✅ Generate internal distribution label
126+
full_label = get_readable_table_name(table_obj=Table.objects.get(name=name))
127+
if full_label:
128+
return full_label
129+
else:
130+
return None
131+
132+
133+
class ScenarioBundleScenarioDatasetSerializer(serializers.Serializer):
134+
scenario_bundle = serializers.UUIDField(
135+
required=True
136+
) # Validate the scenario bundle UUID
137+
scenario = serializers.UUIDField(required=True) # Validate the scenario UUID
138+
datasets = serializers.ListField(
139+
child=DatasetSerializer(), required=True
140+
) # List of datasets with 'name' and 'type'
141+
142+
# Custom validation for 'scenario'
143+
def validate_scenario(self, value):
144+
try:
145+
UUID(str(value))
146+
except ValueError:
147+
raise serializers.ValidationError("Invalid UUID format for scenario.")
148+
149+
return value
150+
151+
# Custom validation for the entire dataset list
152+
def validate_dataset(self, value):
153+
if not value:
154+
raise serializers.ValidationError("The dataset list cannot be empty.")
155+
156+
# Check for duplicates in dataset names
157+
dataset_names = [dataset["name"] for dataset in value]
158+
if len(dataset_names) != len(set(dataset_names)):
159+
raise serializers.ValidationError("Dataset names must be unique.")
160+
161+
return value

api/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,9 @@
203203
views.ScenarioDataTablesListAPIView.as_view(),
204204
name="list-scenario-datasets",
205205
),
206+
re_path(
207+
r"^v0/scenario-bundle/scenario/manage-datasets/?$",
208+
views.ManageOekgScenarioDatasets.as_view(),
209+
name="add-scenario-datasets",
210+
),
206211
]

api/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Collection of utility functions for the API used to define various action
3+
like processing steps.
4+
"""
5+
6+
from oekg.sparqlModels import DatasetConfig
7+
8+
9+
def get_dataset_configs(validated_data) -> list[DatasetConfig]:
10+
"""Converts validated serializer data into a list of DatasetConfig objects."""
11+
return [
12+
DatasetConfig.from_serializer_data(validated_data, dataset_entry)
13+
for dataset_entry in validated_data["datasets"]
14+
]

api/views.py

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
from omi.dialects.oep.compiler import JSONCompiler
3232
from omi.structure import OEPMetadata
3333
from rest_framework import generics, status
34+
from rest_framework.permissions import IsAuthenticated
35+
36+
# views.py
37+
from rest_framework.response import Response
3438
from rest_framework.views import APIView
3539

3640
import api.parser
@@ -43,13 +47,19 @@
4347
from api.serializers import (
4448
EnergyframeworkSerializer,
4549
EnergymodelSerializer,
50+
ScenarioBundleScenarioDatasetSerializer,
4651
ScenarioDataTablesSerializer,
4752
)
53+
from api.utils import get_dataset_configs
4854
from dataedit.models import Embargo
4955
from dataedit.models import Schema as DBSchema
5056
from dataedit.models import Table as DBTable
5157
from dataedit.views import get_tag_keywords_synchronized_metadata, schema_whitelist
58+
from factsheet.permission_decorator import post_only_if_user_is_owner_of_scenario_bundle
5259
from modelview.models import Energyframework, Energymodel
60+
61+
# from oekg.sparqlQuery import remove_datasets_from_scenario
62+
from oekg.utils import process_datasets_sparql_query
5363
from oeplatform.settings import PLAYGROUNDS, UNVERSIONED_SCHEMAS, USE_LOEP, USE_ONTOP
5464

5565
if USE_LOEP:
@@ -244,11 +254,11 @@ class Sequence(APIView):
244254
@api_exception
245255
def put(self, request, schema, sequence):
246256
if schema not in PLAYGROUNDS and schema not in UNVERSIONED_SCHEMAS:
247-
raise APIError('Schema is not in allowed set of schemes for upload')
257+
raise APIError("Schema is not in allowed set of schemes for upload")
248258
if schema.startswith("_"):
249-
raise APIError('Schema starts with _, which is not allowed')
259+
raise APIError("Schema starts with _, which is not allowed")
250260
if request.user.is_anonymous:
251-
raise APIError('User is anonymous', 401)
261+
raise APIError("User is anonymous", 401)
252262
if actions.has_table(dict(schema=schema, sequence_name=sequence), {}):
253263
raise APIError("Sequence already exists", 409)
254264
return self.__create_sequence(request, schema, sequence, request.data)
@@ -257,11 +267,11 @@ def put(self, request, schema, sequence):
257267
@require_delete_permission
258268
def delete(self, request, schema, sequence):
259269
if schema not in PLAYGROUNDS and schema not in UNVERSIONED_SCHEMAS:
260-
raise APIError('Schema is not in allowed set of schemes for upload')
270+
raise APIError("Schema is not in allowed set of schemes for upload")
261271
if schema.startswith("_"):
262-
raise APIError('Schema starts with _, which is not allowed')
272+
raise APIError("Schema starts with _, which is not allowed")
263273
if request.user.is_anonymous:
264-
raise APIError('User is anonymous', 401)
274+
raise APIError("User is anonymous", 401)
265275
return self.__delete_sequence(request, schema, sequence, request.data)
266276

267277
@load_cursor()
@@ -371,9 +381,9 @@ def post(self, request, schema, table):
371381
:return:
372382
"""
373383
if schema not in PLAYGROUNDS and schema not in UNVERSIONED_SCHEMAS:
374-
raise APIError('Schema is not in allowed set of schemes for upload')
384+
raise APIError("Schema is not in allowed set of schemes for upload")
375385
if schema.startswith("_"):
376-
raise APIError('Schema starts with _, which is not allowed')
386+
raise APIError("Schema starts with _, which is not allowed")
377387
json_data = request.data
378388

379389
if "column" in json_data["type"]:
@@ -423,11 +433,11 @@ def put(self, request, schema, table):
423433
:return:
424434
"""
425435
if schema not in PLAYGROUNDS and schema not in UNVERSIONED_SCHEMAS:
426-
raise APIError('Schema is not in allowed set of schemes for upload')
436+
raise APIError("Schema is not in allowed set of schemes for upload")
427437
if schema.startswith("_"):
428-
raise APIError('Schema starts with _, which is not allowed')
438+
raise APIError("Schema starts with _, which is not allowed")
429439
if request.user.is_anonymous:
430-
raise APIError('User is anonymous', 401)
440+
raise APIError("User is anonymous", 401)
431441
if actions.has_table(dict(schema=schema, table=table), {}):
432442
raise APIError("Table already exists", 409)
433443
json_data = request.data.get("query", {})
@@ -967,10 +977,10 @@ def get(self, request, schema, table, row_id=None):
967977
content_type="text/csv",
968978
session=session,
969979
)
970-
response["Content-Disposition"] = (
971-
'attachment; filename="{schema}__{table}.csv"'.format(
972-
schema=schema, table=table
973-
)
980+
response[
981+
"Content-Disposition"
982+
] = 'attachment; filename="{schema}__{table}.csv"'.format(
983+
schema=schema, table=table
974984
)
975985
return response
976986
elif format == "datapackage":
@@ -998,10 +1008,10 @@ def get(self, request, schema, table, row_id=None):
9981008
content_type="application/zip",
9991009
session=session,
10001010
)
1001-
response["Content-Disposition"] = (
1002-
'attachment; filename="{schema}__{table}.zip"'.format(
1003-
schema=schema, table=table
1004-
)
1011+
response[
1012+
"Content-Disposition"
1013+
] = 'attachment; filename="{schema}__{table}.zip"'.format(
1014+
schema=schema, table=table
10051015
)
10061016
return response
10071017
else:
@@ -1574,3 +1584,56 @@ class ScenarioDataTablesListAPIView(generics.ListAPIView):
15741584
topic = "scenario"
15751585
queryset = DBTable.objects.filter(schema__name=topic)
15761586
serializer_class = ScenarioDataTablesSerializer
1587+
1588+
1589+
class ManageOekgScenarioDatasets(APIView):
1590+
permission_classes = [IsAuthenticated] # Require authentication
1591+
1592+
@post_only_if_user_is_owner_of_scenario_bundle
1593+
def post(self, request):
1594+
serializer = ScenarioBundleScenarioDatasetSerializer(data=request.data)
1595+
if not serializer.is_valid():
1596+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
1597+
1598+
try:
1599+
dataset_configs = get_dataset_configs(serializer.validated_data)
1600+
response_data = process_datasets_sparql_query(dataset_configs)
1601+
except APIError as e:
1602+
return Response({"error": str(e)}, status=e.status)
1603+
except Exception:
1604+
return Response({"error": "An unexpected error occurred."}, status=500)
1605+
1606+
if "error" in response_data:
1607+
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
1608+
1609+
return Response(response_data, status=status.HTTP_200_OK)
1610+
1611+
# @post_only_if_user_is_owner_of_scenario_bundle
1612+
# def delete(self, request):
1613+
# serializer = ScenarioBundleScenarioDatasetSerializer(data=request.data)
1614+
# if serializer.is_valid():
1615+
# scenario_uuid = serializer.validated_data["scenario"]
1616+
# datasets = serializer.validated_data["datasets"]
1617+
1618+
# # Iterate over each dataset to process it properly
1619+
# for dataset in datasets:
1620+
# dataset_name = dataset["name"]
1621+
# dataset_type = dataset["type"]
1622+
1623+
# # Remove the dataset from the scenario in the bundle
1624+
# success = remove_datasets_from_scenario(
1625+
# scenario_uuid, dataset_name, dataset_type
1626+
# )
1627+
1628+
# if not success:
1629+
# return Response(
1630+
# {"error": f"Failed to remove dataset {dataset_name}"},
1631+
# status=status.HTTP_400_BAD_REQUEST,
1632+
# )
1633+
1634+
# return Response(
1635+
# {"message": "Datasets removed successfully"},
1636+
# status=status.HTTP_200_OK,
1637+
# )
1638+
1639+
# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

dataedit/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
IntegerField,
1717
JSONField,
1818
)
19+
from django.urls import reverse
1920
from django.utils import timezone
2021

2122
# Create your models here.
@@ -77,6 +78,9 @@ class Table(Tagable):
7778
is_publish = BooleanField(null=False, default=False)
7879
human_readable_name = CharField(max_length=1000, null=True)
7980

81+
def get_absolute_url(self):
82+
return reverse("dataedit:view", kwargs={"pk": self.pk})
83+
8084
@classmethod
8185
def load(cls, schema, table):
8286
"""
@@ -719,5 +723,6 @@ def filter_opr_by_table(schema, table):
719723
"""
720724
return PeerReview.objects.filter(schema=schema, table=table)
721725

726+
@staticmethod
722727
def filter_opr_by_id(opr_id):
723728
return PeerReview.objects.filter(id=opr_id).first()

dataedit/views.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2355,9 +2355,6 @@ def post(self, request, schema, table, review_id):
23552355
Handle POST requests for contributor's review. Merges and updates
23562356
the review data in the PeerReview table.
23572357
2358-
Missing parts:
2359-
- merge contributor field review and reviewer field review
2360-
23612358
Args:
23622359
request (HttpRequest): The incoming HTTP POST request.
23632360
schema (str): The schema of the table.
@@ -2367,9 +2364,6 @@ def post(self, request, schema, table, review_id):
23672364
Returns:
23682365
HttpResponse: Rendered HTML response for contributor review.
23692366
2370-
Note:
2371-
This method has some missing parts regarding the merging of contributor
2372-
and reviewer field review.
23732367
"""
23742368

23752369
context = {}

0 commit comments

Comments
 (0)