Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9473f24
added decorator for edit and read viewer rights
josihoppe May 26, 2026
f760a6b
exchanged repeating check for viewer share rights with decorator in p…
josihoppe May 26, 2026
0a23226
fixed worng variable name in decorator
josihoppe May 27, 2026
468f8e9
added different return view_func behavior for views with different pa…
josihoppe May 27, 2026
b7c7335
added decorators and user.has_edit_rights checks to more complex view…
josihoppe May 27, 2026
7ebdd92
exchanged repeating checks for viewer share rigths with decorator in …
josihoppe May 27, 2026
de57cc2
removed doubled viewer right check
josihoppe May 27, 2026
cabefa4
added new decorator if just the owner of project has permission
josihoppe May 27, 2026
cfe7877
added is owner decorator to project views
josihoppe Jun 3, 2026
daf9335
renamed decorators
josihoppe Jun 3, 2026
45c5a6e
added another shorter check
josihoppe Jun 3, 2026
628ed72
changed permission for seeing t and updating imeseries graph to edit …
josihoppe Jun 3, 2026
d6111d4
changed permission for duplicating a shared scenario in own project o…
josihoppe Jun 3, 2026
90bd012
changed permission for deleting project in own project overview to re…
josihoppe Jun 3, 2026
25456e5
removed permission check in get_timeseries since only users with perm…
josihoppe Jun 9, 2026
4dfa132
added error message page for page requests without proper permission
josihoppe Jun 10, 2026
7946f49
fixed delete function, only owner deletes in database, viewer remove …
josihoppe Jun 16, 2026
4e2fbc4
added tests for project delete funktion in with different permissions
josihoppe Jun 16, 2026
68217bb
fixed showing of delete button when viewer with read rights
josihoppe Jun 16, 2026
1f8519f
project options only show if viewer with edit rights or owner
josihoppe Jun 16, 2026
b30c58e
changed message for deleting only from own project list
josihoppe Jun 16, 2026
f529467
added tests for scenario duplicate permissions
josihoppe Jun 16, 2026
7faad19
Add translation strings for sharing rights messages
paulapreuss Jun 18, 2026
0919670
fixed showing delete button, template filter needed to be adjusted to…
josihoppe Jun 23, 2026
f2e3a5a
fix permission for starting and restarting simulationons, now not pos…
josihoppe Jun 23, 2026
efa6795
Update changelog
paulapreuss Jul 1, 2026
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
2 changes: 2 additions & 0 deletions app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## [Unreleased]
### Fixed
- Fix issues with read and edit rights on shared projects [[#478](https://github.com/open-plan-tool/gui/pull/478)]

## [v2.1.4] – 2026-07-01
### Fixed
Expand Down
90 changes: 17 additions & 73 deletions app/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
get_selected_scenarios_in_cache,
)

from projects.decorators import user_has_edit_rights, user_has_read_rights
from projects.forms import BusForm, AssetCreateForm, StorageForm

from projects.constants import COMPARE_VIEW, STEP_LIST, MAX_STEP
Expand Down Expand Up @@ -72,13 +73,9 @@
@login_required
@json_view
@require_http_methods(["GET"])
@user_has_read_rights
def scenario_available_results(request, scen_id):
scenario = get_object_or_404(Scenario, pk=scen_id)
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

try:
assets_results_obj = AssetsResults.objects.get(simulation=scenario.simulation)
Expand Down Expand Up @@ -174,9 +171,7 @@ def scenario_visualize_results(
)
else:
project = get_object_or_404(Project, id=proj_id)
if (project.user != request.user) and (
project.viewers.filter(user__email=request.user.email).exists() is False
):
if not request.user.has_read_rights(project):
raise PermissionDenied

selected_scenarios = get_selected_scenarios_in_cache(request, proj_id)
Expand Down Expand Up @@ -217,12 +212,6 @@ def scenario_visualize_results(
scenario = get_object_or_404(Scenario, id=scen_id)
# TODO: change this when multi-scenario selection is allowed

if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

qs = FancyResults.objects.filter(simulation=scenario.simulation)

if qs.exists() and scenario in user_scenarios:
Expand Down Expand Up @@ -271,6 +260,7 @@ def scenario_visualize_results(

@login_required
@require_http_methods(["POST", "GET"])
@user_has_read_rights
def project_compare_results(request, proj_id, step_id=5, max_step=MAX_STEP):
request.session[COMPARE_VIEW] = True
user_projects = fetch_user_projects(request.user)
Expand All @@ -281,10 +271,6 @@ def project_compare_results(request, proj_id, step_id=5, max_step=MAX_STEP):
)

project = get_object_or_404(Project, id=proj_id)
if (project.user != request.user) and (
project.viewers.filter(user__email=request.user.email).exists() is False
):
raise PermissionDenied

user_scenarios = project.get_scenarios_with_results()
report_items_data = [
Expand Down Expand Up @@ -340,10 +326,7 @@ def project_sensitivity_analysis(request, proj_id, sa_id=None):
)
else:
project = get_object_or_404(Project, id=proj_id)
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
if not request.user.has_read_rights(project):
raise PermissionDenied

user_sa = get_project_sensitivity_analysis(project)
Expand Down Expand Up @@ -1002,6 +985,7 @@ def view_asset_parameters(request, scen_id, asset_type_name, asset_uuid):
@login_required
@json_view
@require_http_methods(["GET"])
@user_has_read_rights
def scenario_economic_results(request, scen_id=None):
"""
This view gathers all simulation specific cost matrix KPI results
Expand All @@ -1017,14 +1001,6 @@ def scenario_economic_results(request, scen_id=None):

scenario = get_object_or_404(Scenario, pk=scen_id)

# if scenario.project.user != request.user:
# return HttpResponseForbidden()
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

try:
kpi_cost_results_obj = KPICostsMatrixResults.objects.get(
simulation=scenario.simulation
Expand Down Expand Up @@ -1093,6 +1069,7 @@ def scenario_economic_results(request, scen_id=None):
@login_required
@json_view
@require_http_methods(["GET"])
@user_has_read_rights
def scenario_visualize_timeseries(request, proj_id=None, scen_id=None):
if scen_id is None:
selected_scenario = get_selected_scenarios_in_cache(request, proj_id)
Expand All @@ -1103,11 +1080,6 @@ def scenario_visualize_timeseries(request, proj_id=None, scen_id=None):

for scen_id in selected_scenario:
scenario = get_object_or_404(Scenario, pk=scen_id)
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied
simulations.append(scenario.simulation)

results_json = report_item_render_to_json(
Expand All @@ -1122,13 +1094,10 @@ def scenario_visualize_timeseries(request, proj_id=None, scen_id=None):
)


@login_required
@user_has_read_rights
def scenario_visualize_stacked_timeseries(request, scen_id):
scenario = get_object_or_404(Scenario, pk=scen_id)
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

results_json = []
for energy_vector in scenario.energy_vectors:
Expand All @@ -1151,6 +1120,7 @@ def scenario_visualize_stacked_timeseries(request, scen_id):


# TODO exclude sink components
@user_has_read_rights
def scenario_visualize_capacities(request, proj_id, scen_id=None):
if scen_id is None:
selected_scenario = get_selected_scenarios_in_cache(request, proj_id)
Expand All @@ -1161,11 +1131,6 @@ def scenario_visualize_capacities(request, proj_id, scen_id=None):

qs = Scenario.objects.filter(id__in=selected_scenario).order_by("name")
for scenario in qs:
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied
simulations.append(scenario.simulation)

results_json = report_item_render_to_json(
Expand All @@ -1180,6 +1145,7 @@ def scenario_visualize_capacities(request, proj_id, scen_id=None):
)


@user_has_read_rights
def scenario_visualize_costs(request, proj_id, scen_id=None):
if scen_id is None:
selected_scenario = get_selected_scenarios_in_cache(request, proj_id)
Expand All @@ -1190,11 +1156,6 @@ def scenario_visualize_costs(request, proj_id, scen_id=None):

qs = Scenario.objects.filter(id__in=selected_scenario).order_by("name")
for scenario in qs:
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied
simulations.append(scenario.simulation)

results_json = []
Expand All @@ -1216,13 +1177,11 @@ def scenario_visualize_costs(request, proj_id, scen_id=None):


# TODO: Sector coupling must be refined (including transformer flows)
@login_required
@user_has_read_rights
def scenario_visualize_sankey(request, scen_id, ts=None):
scenario = get_object_or_404(Scenario, pk=scen_id)
if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

if ts is not None:
ts = int(ts)
results_json = report_item_render_to_json(
Expand All @@ -1243,15 +1202,10 @@ def scenario_visualize_sankey(request, scen_id, ts=None):

@login_required
@require_http_methods(["GET"])
@user_has_read_rights
def download_scalar_results(request, scen_id):
scenario = get_object_or_404(Scenario, pk=scen_id)

if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

try:
kpi_scalar_results_obj = KPIScalarResults.objects.get(
simulation=scenario.simulation
Expand Down Expand Up @@ -1290,15 +1244,10 @@ def download_scalar_results(request, scen_id):

@login_required
@require_http_methods(["GET"])
@user_has_read_rights
def download_cost_results(request, scen_id):
scenario = get_object_or_404(Scenario, pk=scen_id)

if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

try:
kpi_cost_results_obj = KPICostsMatrixResults.objects.get(
simulation=scenario.simulation
Expand Down Expand Up @@ -1336,15 +1285,10 @@ def download_cost_results(request, scen_id):

@login_required
@require_http_methods(["GET"])
@user_has_read_rights
def download_timeseries_results(request, scen_id):
scenario = get_object_or_404(Scenario, pk=scen_id)

if (scenario.project.user != request.user) and (
scenario.project.viewers.filter(user__email=request.user.email).exists()
is False
):
raise PermissionDenied

try:
assets_results_obj = AssetsResults.objects.get(simulation=scenario.simulation)
assets_results_json = json.loads(assets_results_obj.assets_list)
Expand Down
15 changes: 15 additions & 0 deletions app/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,21 @@ msgstr ""
"Wählen Sie den/die Benutzer aus, für den/die Sie die Zugriffsrechte "
"entziehen möchten."

msgid "Revoke access"
msgstr "Zugriffsrechte entziehen"

msgid "Share"
msgstr "Teilen"

msgid "You need to be the owner of the project to access this."
msgstr "Sie müssen der Eigentümer des Projekts sein, um darauf zugreifen zu können."

msgid "You need to be the owner of the project or have read rights to access this."
msgstr "Sie müssen entweder der Eigentümer des Projekts sein oder über Leserechte verfügen, um darauf zugreifen zu können."

msgid "You need to be the owner of the project or have edit rights to access this."
msgstr "Sie müssen der Eigentümer des Projekts sein oder über Bearbeitungsrechte verfügen, um darauf zugreifen zu können."

#: projects/forms.py:367
msgid "Users currently having access to the project"
msgstr "Benutzer, die derzeit Zugriff auf das Projekt haben"
Expand Down
106 changes: 106 additions & 0 deletions app/projects/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext as _

from functools import wraps

from projects.models import Project, Scenario


def get_project_from_proj_or_scen_id(proj_id, scen_id):
if proj_id is not None:
return get_object_or_404(Project, pk=proj_id)

if scen_id is not None:
scenario = get_object_or_404(Scenario, pk=scen_id)
return scenario.project

raise ValueError("this decorator requires proj_id or scen_id")


def user_is_owner(view_func):
@wraps(view_func)
def _wrapped_view(request, proj_id=None, scen_id=None, *args, **kwargs):
project = get_project_from_proj_or_scen_id(proj_id, scen_id)

if project.user != request.user:
reason = _("You need to be the owner of the project to access this.")
return render(
request,
"error_403.html",
{
"reason": reason,
},
)

# check for existing parameters to handle the different view parameters
if proj_id is not None and scen_id is not None:
return view_func(request, proj_id, scen_id, *args, **kwargs)
elif proj_id is not None:
return view_func(request, proj_id, *args, **kwargs)
elif scen_id is not None:
return view_func(request, scen_id, *args, **kwargs)

return _wrapped_view


def user_has_read_rights(view_func):
@wraps(view_func)
def _wrapped_view(request, proj_id=None, scen_id=None, *args, **kwargs):
project = get_project_from_proj_or_scen_id(proj_id, scen_id)

if (project.user != request.user) and (
project.viewers.filter(user__email=request.user.email).exists() is False
):
reason = _(
"You need to be the owner of the project or have read rights to access this."
)
return render(
request,
"error_403.html",
{
"reason": reason,
},
)

# check for existing parameters to handle the different view parameters
if proj_id is not None and scen_id is not None:
return view_func(request, proj_id, scen_id, *args, **kwargs)
elif proj_id is not None:
return view_func(request, proj_id, *args, **kwargs)
elif scen_id is not None:
return view_func(request, scen_id, *args, **kwargs)

return _wrapped_view


def user_has_edit_rights(view_func):
@wraps(view_func)
def _wrapped_view(request, proj_id=None, scen_id=None, *args, **kwargs):
project = get_project_from_proj_or_scen_id(proj_id, scen_id)

if (project.user != request.user) and (
project.viewers.filter(
user__email=request.user.email, share_rights="edit"
).exists()
is False
):
reason = _(
"You need to be the owner of the project or have edit rights to access this."
)
return render(
request,
"error_403.html",
{
"reason": reason,
},
)

# check for existing parameters to handle the different view parameters
if proj_id is not None and scen_id is not None:
return view_func(request, proj_id, scen_id, *args, **kwargs)
elif proj_id is not None:
return view_func(request, proj_id, *args, **kwargs)
elif scen_id is not None:
return view_func(request, scen_id, *args, **kwargs)

return _wrapped_view
4 changes: 1 addition & 3 deletions app/projects/templatetags/custom_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ def has_viewer_read_rights(proj_id, user):
if project.user == user:
answer = True
else:
qs = project.viewers.filter(
Q(user__email=user.email) & Q(share_rights="read")
)
qs = project.viewers.filter(Q(user__email=user.email))
if qs.exists():
answer = True
return answer
Expand Down
Loading
Loading