Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@
admin.site.register(Asset)
admin.site.register(Bus)
admin.site.register(ConnectionLink)


class AssetTemplateAdmin(admin.ModelAdmin):
list_display = ("id", "visibility", "name", "asset_type")
list_filter = ("visibility", "asset_type")


admin.site.register(AssetTemplate, AssetTemplateAdmin)
70 changes: 70 additions & 0 deletions app/projects/migrations/0026_assettemplate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Generated by Django 4.2.4 on 2025-10-21 11:43

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),
("projects", "0025_connectionport"),
]

operations = [
migrations.CreateModel(
name="AssetTemplate",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("desc", models.TextField(blank=True)),
(
"visibility",
models.CharField(
choices=[
("project", "Project"),
("account", "Account"),
("global", "Everyone"),
],
max_length=8,
),
),
("created_ts", models.DateTimeField(auto_now_add=True)),
("parameters", models.JSONField()),
(
"asset_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="projects.assettype",
),
),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"project",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="projects.project",
),
),
],
),
]
26 changes: 26 additions & 0 deletions app/projects/models/base_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,9 @@ def remove_field(self, field_name):
temp.pop(temp.index(field_name))
self.asset_fields = "[" + ",".join(temp) + "]"

def __str__(self):
return self.asset_type


class TopologyNode(models.Model):
name = models.CharField(max_length=60, null=False, blank=False)
Expand Down Expand Up @@ -749,6 +752,29 @@ def is_input_timeseries_empty(self):
return self.input_timeseries == ""


class AssetTemplate(models.Model):
VISIBILITY_CHOICES = [
("project", "Project"),
("account", "Account"),
("global", "Everyone"),
]
name = models.CharField(max_length=255)
desc = models.TextField(blank=True)
project = models.ForeignKey(
Project, on_delete=models.SET_NULL, null=True, blank=True
)
visibility = models.CharField(max_length=8, choices=VISIBILITY_CHOICES)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
created_ts = models.DateTimeField(auto_now_add=True)
asset_type = models.ForeignKey(AssetType, on_delete=models.CASCADE)
parameters = models.JSONField()


class COPCalculator(models.Model):

scenario = models.ForeignKey(
Expand Down
48 changes: 42 additions & 6 deletions app/projects/scenario_topology_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from projects.models import (
Bus,
AssetType,
AssetTemplate,
Scenario,
ConnectionLink,
Asset,
Expand All @@ -18,10 +19,13 @@
)
import json
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.db.models import Value
from django.db.models.functions import Concat
from projects.forms import AssetCreateForm, BusForm, StorageForm
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _


# region sent db nodes to js
from django.http import JsonResponse
import logging
Expand Down Expand Up @@ -285,10 +289,15 @@ def handle_storage_unit_form_post(
return JsonResponse({"success": False, "exception": ex}, status=422)

logger.warning(f"The submitted asset has erroneous field values.")
form_html = get_template("asset/storage_asset_create_form.html")
return JsonResponse(
{"success": False, "form_html": form_html.render({"form": form})}, status=422
form_html = get_template("asset/storage_asset_create_form.html").render(
{
"form": form,
"templates": get_available_templates(
asset_type_name, scenario.project, request.user
),
}
)
return JsonResponse({"success": False, "form_html": form_html}, status=422)


def handle_asset_form_post(request, scen_id=0, asset_type_name="", asset_uuid=None):
Expand Down Expand Up @@ -365,10 +374,15 @@ def handle_asset_form_post(request, scen_id=0, asset_type_name="", asset_uuid=No
return JsonResponse({"success": True, "asset_id": asset.unique_id}, status=200)
logger.warning(f"The submitted asset has erroneous field values.")

form_html = get_template("asset/asset_create_form.html")
return JsonResponse(
{"success": False, "form_html": form_html.render({"form": form})}, status=422
form_html = get_template("asset/asset_create_form.html").render(
{
"form": form,
"templates": get_available_templates(
asset_type_name, scenario.project, request.user
),
}
)
return JsonResponse({"success": False, "form_html": form_html}, status=422)


def load_scenario_topology_from_db(scen_id):
Expand Down Expand Up @@ -884,3 +898,25 @@ def create_ESS_objects(all_ess_assets_node_list, scen_id):
if asset.name == "capacity":
# check if there is a connection link to a bus
pass


def get_available_templates(asset_type_name, project, user):
# collect available templates
asset_templates = AssetTemplate.objects.filter(
asset_type__asset_type=asset_type_name
).order_by("created_ts")
# project templates
project_templates = asset_templates.filter(
visibility="project", project=project
).annotate(display_name=Concat("name", Value(" (prj)")))
# account templates
account_templates = asset_templates.filter(
visibility="account", created_by=user
).annotate(display_name=Concat("name", Value(" (acc)")))
# global templates
global_templates = asset_templates.filter(visibility="global").annotate(
display_name=Concat("name", Value(" (std)"))
)
return AssetTemplate.objects.none().union(
project_templates, account_templates, global_templates
)
8 changes: 7 additions & 1 deletion app/projects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
name="get_constant_timeseries_id",
),
re_path(
"get/constant/timeseries/id/(?P<ts_length>\d+)/value/(?P<value>\d+(\.\d+)?)/$",
r"get/constant/timeseries/id/(?P<ts_length>\d+)/value/(?P<value>\d+(\.\d+)?)/$",
get_constant_timeseries_id,
name="get_constant_timeseries_id",
),
Expand Down Expand Up @@ -210,6 +210,12 @@
asset_cops_create_or_update,
name="asset_cops_create_or_update",
),
# templates
path(
"project/<int:project_id>/template/",
template_get_or_create,
name="template_get_or_create",
),
# ParameterChangeTracker (track of simulated scenario changes)
path(
"reset_scenario_changes/<int:scen_id>",
Expand Down
50 changes: 48 additions & 2 deletions app/projects/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
Comment,
ConnectionLink,
AssetType,
AssetTemplate,
UseCase,
Scenario,
Simulation,
Expand Down Expand Up @@ -64,6 +65,7 @@
duplicate_scenario_connections,
load_scenario_from_dict,
load_project_from_dict,
get_available_templates,
)
from projects.helpers import format_scenario_for_mvs, PARAMETERS
from dashboard.helpers import fetch_user_projects
Expand Down Expand Up @@ -1618,7 +1620,7 @@ def get_asset_create_form(request, scen_id=0, asset_type_name="", asset_uuid=Non
)
return render(request, "asset/bus_create_form.html", {"form": form})

elif asset_type_name in ["bess", "h2ess", "gess", "hess"]:
if asset_type_name in ["bess", "h2ess", "gess", "hess"]:
if asset_uuid:
existing_ess_asset = get_object_or_404(Asset, unique_id=asset_uuid)
ess_asset_children = Asset.objects.filter(
Expand Down Expand Up @@ -1669,7 +1671,16 @@ def get_asset_create_form(request, scen_id=0, asset_type_name="", asset_uuid=Non
input_output_mapping=input_output_mapping,
initial={"name": default_name},
)
return render(request, "asset/storage_asset_create_form.html", {"form": form})
return render(
request,
"asset/storage_asset_create_form.html",
{
"form": form,
"templates": get_available_templates(
asset_type_name, scenario.project, request.user
),
},
)
else: # all other assets

if asset_uuid:
Expand Down Expand Up @@ -1703,6 +1714,9 @@ def get_asset_create_form(request, scen_id=0, asset_type_name="", asset_uuid=Non

context = {
"form": form,
"templates": get_available_templates(
asset_type_name, scenario.project, request.user
),
"asset_type_name": asset_type_name,
"input_timeseries_data": input_timeseries_data,
"input_timeseries_timestamps": json.dumps(
Expand Down Expand Up @@ -1845,6 +1859,38 @@ def asset_cops_create_or_update(
# endregion Asset


# templates
@login_required
@require_http_methods(["GET", "POST"])
def template_get_or_create(request, project_id):
if request.method == "GET":
template = get_object_or_404(AssetTemplate, id=int(request.GET.get("id")))
# check permissions
if template.visibility == "project" and project_id != template.project_id:
raise PermissionDenied
if template.visibility == "account" and request.user != template.created_by:
raise PermissionDenied
# visibility = global needs no check
return JsonResponse(template.parameters)

# POST: create new template
asset_type = get_object_or_404(AssetType, asset_type=request.POST["asset_type"])
template = AssetTemplate.objects.create(
name=request.POST["name"],
desc=request.POST["desc"],
project_id=project_id,
visibility=request.POST["visibility"],
created_by=request.user,
asset_type=asset_type,
parameters=json.loads(request.POST["data"]),
)
if request.POST["request_global"] == "true":
logger.warning(
f"AssetTemplate #{template.id} ({template.name}) should be made public"
)
return HttpResponse(status=201) # created


# region MVS JSON Related


Expand Down
Loading