diff --git a/envipath/settings.py b/envipath/settings.py index 35ddc697..87865f4e 100644 --- a/envipath/settings.py +++ b/envipath/settings.py @@ -92,10 +92,19 @@ if os.environ.get("REGISTRATION_MANDATORY", False) == "True": ROOT_URLCONF = "envipath.urls" +TEMPLATE_DIRS = [ + os.path.join(BASE_DIR, "templates"), +] + +# If we have a non-public tenant, we might need to overwrite some templates +# search TENANT folder first... +if TENANT != "public": + TEMPLATE_DIRS.insert(0, os.path.join(BASE_DIR, TENANT, "templates")) + TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": (os.path.join(BASE_DIR, "templates"),), + "DIRS": TEMPLATE_DIRS, "APP_DIRS": True, "OPTIONS": { "context_processors": [ diff --git a/epapi/tests/v1/test_scenario_creation.py b/epapi/tests/v1/test_scenario_creation.py index 735bae12..9ed66fa9 100644 --- a/epapi/tests/v1/test_scenario_creation.py +++ b/epapi/tests/v1/test_scenario_creation.py @@ -60,7 +60,7 @@ class ScenarioCreationAPITests(TestCase): ) self.assertEqual(response.status_code, 404) - self.assertIn("Package not found", response.json()["detail"]) + self.assertIn(f"Package with UUID {fake_uuid} not found", response.json()["detail"]) def test_create_scenario_insufficient_permissions(self): """Test that unauthorized access returns 403.""" diff --git a/epapi/v1/dal.py b/epapi/v1/dal.py index ec537c86..98ee342a 100644 --- a/epapi/v1/dal.py +++ b/epapi/v1/dal.py @@ -41,6 +41,24 @@ def get_package_for_read(user, package_uuid: UUID): return package +def get_package_for_write(user, package_uuid: UUID): + """ + Get package by UUID with permission check. + """ + + # FIXME: update package manager with custom exceptions to avoid manual checks here + try: + package = Package.objects.get(uuid=package_uuid) + except Package.DoesNotExist: + raise EPAPINotFoundError(f"Package with UUID {package_uuid} not found") + + # FIXME: optimize package manager to exclusively work with UUIDs + if not user or user.is_anonymous or not PackageManager.writable(user, package): + raise EPAPIPermissionDeniedError("Insufficient permissions to access this package.") + + return package + + def get_scenario_for_read(user, scenario_uuid: UUID): """Get scenario by UUID with read permission check.""" try: diff --git a/epapi/v1/endpoints/scenarios.py b/epapi/v1/endpoints/scenarios.py index 27b5df1b..21a22e1a 100644 --- a/epapi/v1/endpoints/scenarios.py +++ b/epapi/v1/endpoints/scenarios.py @@ -9,7 +9,6 @@ import logging import json from epdb.models import Scenario -from epdb.logic import PackageManager from epdb.views import _anonymous_or_real from ..pagination import EnhancedPageNumberPagination from ..schemas import ( @@ -17,7 +16,7 @@ from ..schemas import ( ScenarioOutSchema, ScenarioCreateSchema, ) -from ..dal import get_user_entities_for_read, get_package_entities_for_read +from ..dal import get_user_entities_for_read, get_package_entities_for_read, get_package_for_write from envipy_additional_information import registry logger = logging.getLogger(__name__) @@ -58,7 +57,7 @@ def create_scenario(request, package_uuid: UUID, payload: ScenarioCreateSchema = user = _anonymous_or_real(request) try: - current_package = PackageManager.get_package_by_id(user, package_uuid) + current_package = get_package_for_write(user, package_uuid) except ValueError as e: error_msg = str(e) if "does not exist" in error_msg: diff --git a/epdb/legacy_api.py b/epdb/legacy_api.py index aba9ee08..f55a6c82 100644 --- a/epdb/legacy_api.py +++ b/epdb/legacy_api.py @@ -1392,7 +1392,7 @@ def create_package_scenario(request, package_uuid): study_type = request.POST.get("type") ais = [] - types = request.POST.getlist("adInfoTypes[]") + types = request.POST.get("adInfoTypes[]", "").split(",") for t in types: ais.append(build_additional_information_from_request(request, t)) diff --git a/epdb/migrations/0020_alter_compoundstructure_options_and_more.py b/epdb/migrations/0020_alter_compoundstructure_options_and_more.py new file mode 100644 index 00000000..13481d34 --- /dev/null +++ b/epdb/migrations/0020_alter_compoundstructure_options_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.7 on 2026-03-09 10:41 + +import django.db.models.deletion +from django.db import migrations, models + + +def populate_polymorphic_ctype(apps, schema_editor): + ContentType = apps.get_model("contenttypes", "ContentType") + Compound = apps.get_model("epdb", "Compound") + CompoundStructure = apps.get_model("epdb", "CompoundStructure") + + # Update Compound records + compound_ct = ContentType.objects.get_for_model(Compound) + Compound.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=compound_ct) + + # Update CompoundStructure records + compound_structure_ct = ContentType.objects.get_for_model(CompoundStructure) + CompoundStructure.objects.filter(polymorphic_ctype__isnull=True).update( + polymorphic_ctype=compound_structure_ct + ) + + +def reverse_populate_polymorphic_ctype(apps, schema_editor): + Compound = apps.get_model("epdb", "Compound") + CompoundStructure = apps.get_model("epdb", "CompoundStructure") + + Compound.objects.all().update(polymorphic_ctype=None) + CompoundStructure.objects.all().update(polymorphic_ctype=None) + + +class Migration(migrations.Migration): + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("epdb", "0019_remove_scenario_additional_information_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="compoundstructure", + options={"base_manager_name": "objects"}, + ), + migrations.AddField( + model_name="compound", + name="polymorphic_ctype", + field=models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="polymorphic_%(app_label)s.%(class)s_set+", + to="contenttypes.contenttype", + ), + ), + migrations.AddField( + model_name="compoundstructure", + name="polymorphic_ctype", + field=models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="polymorphic_%(app_label)s.%(class)s_set+", + to="contenttypes.contenttype", + ), + ), + migrations.RunPython(populate_polymorphic_ctype, reverse_populate_polymorphic_ctype), + ] diff --git a/epdb/models.py b/epdb/models.py index 41b63568..02416265 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -765,7 +765,12 @@ class Package(EnviPathModel): class Compound( - EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin + PolymorphicModel, + EnviPathModel, + AliasMixin, + ScenarioMixin, + ChemicalIdentifierMixin, + AdditionalInformationMixin, ): package = models.ForeignKey( s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True @@ -1095,7 +1100,12 @@ class Compound( class CompoundStructure( - EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin + PolymorphicModel, + EnviPathModel, + AliasMixin, + ScenarioMixin, + ChemicalIdentifierMixin, + AdditionalInformationMixin, ): compound = models.ForeignKey("epdb.Compound", on_delete=models.CASCADE, db_index=True) smiles = models.TextField(blank=False, null=False, verbose_name="SMILES") @@ -4138,7 +4148,7 @@ class Scenario(EnviPathModel): ais = AdditionalInformation.objects.filter(scenario=self) if direct_only: - return ais.filter(content_object__isnull=True) + return ais.filter(object_id__isnull=True) else: return ais diff --git a/fixtures/test_fixtures.jsonl.gz b/fixtures/test_fixtures.jsonl.gz index 4db92bf1..41903129 100644 Binary files a/fixtures/test_fixtures.jsonl.gz and b/fixtures/test_fixtures.jsonl.gz differ diff --git a/fixtures/test_fixtures_incl_model.jsonl.gz b/fixtures/test_fixtures_incl_model.jsonl.gz index 2f24cca5..adcbf217 100644 Binary files a/fixtures/test_fixtures_incl_model.jsonl.gz and b/fixtures/test_fixtures_incl_model.jsonl.gz differ diff --git a/templates/modals/objects/edit_package_permissions_modal.html b/templates/modals/objects/edit_package_permissions_modal.html index cc51937f..66d251ea 100644 --- a/templates/modals/objects/edit_package_permissions_modal.html +++ b/templates/modals/objects/edit_package_permissions_modal.html @@ -71,24 +71,129 @@ - + + + + + + + + + +