diff --git a/api/manage.py b/api/manage.py index fed81fbe..28c4ca52 100755 --- a/api/manage.py +++ b/api/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys diff --git a/api/outdated/conftest.py b/api/outdated/conftest.py index bf9be0d9..ac7c848a 100644 --- a/api/outdated/conftest.py +++ b/api/outdated/conftest.py @@ -22,6 +22,7 @@ register(factories.DependencyFactory) +register(factories.DependencySourceFactory) register(factories.VersionFactory) register(factories.ReleaseVersionFactory) register(factories.ProjectFactory) diff --git a/api/outdated/oidc_auth/permissions.py b/api/outdated/oidc_auth/permissions.py index 55f6e672..24cf747c 100644 --- a/api/outdated/oidc_auth/permissions.py +++ b/api/outdated/oidc_auth/permissions.py @@ -16,12 +16,10 @@ class Request(Protocol): # pragma: no cover """Request protocol for permissions.""" @property - def method(self) -> str: - ... + def method(self) -> str: ... @property - def user(self) -> None | OIDCUser: - ... + def user(self) -> None | OIDCUser: ... class ObjectPermissionIsHasPermission(BasePermission): diff --git a/api/outdated/outdated/factories.py b/api/outdated/outdated/factories.py index c2f0feff..5dafdd01 100644 --- a/api/outdated/outdated/factories.py +++ b/api/outdated/outdated/factories.py @@ -56,20 +56,30 @@ class ProjectFactory(DjangoModelFactory): repo = Sequence(lambda n: "github.com/userorcompany/%s/" % n) repo_type = "public" + class Meta: + model = models.Project + + +class DependencySourceFactory(DjangoModelFactory): + project = SubFactory(ProjectFactory) + path = random.choice( + ["/pyproject.toml", "/api/pyproject.toml", "/ember/pnpm-lock.yaml"] + ) + @post_generation - def versioned_dependencies(self, create, extracted, **kwargs): + def versions(self, create, extracted, **kwargs): if not create: return # pragma: no cover if extracted: - for versioned_dependency in extracted: - self.versioned_dependencies.add(versioned_dependency) + for version in extracted: + self.versions.add(version) class Meta: - model = models.Project + model = models.DependencySource class MaintainerFactory(DjangoModelFactory): - project = SubFactory(ProjectFactory) + source = SubFactory(DependencySourceFactory) user = SubFactory(UserFactory) class Meta: diff --git a/api/outdated/outdated/migrations/0001_initial.py b/api/outdated/outdated/migrations/0001_initial.py index 0e7dd6cb..6f811582 100644 --- a/api/outdated/outdated/migrations/0001_initial.py +++ b/api/outdated/outdated/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.6 on 2024-01-09 09:41 +# Generated by Django 5.0.3 on 2024-03-12 13:54 import uuid @@ -18,7 +18,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="Dependency", + name="DependencySource", fields=[ ( "id", @@ -29,17 +29,39 @@ class Migration(migrations.Migration): serialize=False, ), ), - ("name", models.CharField(max_length=100)), + ("path", models.CharField()), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Project", + fields=[ ( - "provider", + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(db_index=True, max_length=100)), + ("repo", outdated.models.RepositoryURLField(max_length=100)), + ( + "repo_type", models.CharField( - choices=[("PIP", "PIP"), ("NPM", "NPM")], max_length=10 + choices=[ + ("public", "public"), + ("access-token", "access-token"), + ], + max_length=25, ), ), ], options={ "ordering": ["name", "id"], - "unique_together": {("name", "provider")}, }, ), migrations.CreateModel( @@ -57,13 +79,6 @@ class Migration(migrations.Migration): ("major_version", models.IntegerField()), ("minor_version", models.IntegerField()), ("end_of_life", models.DateField(blank=True, null=True)), - ( - "dependency", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="outdated.dependency", - ), - ), ], options={ "ordering": [ @@ -72,7 +87,6 @@ class Migration(migrations.Migration): "major_version", "minor_version", ], - "unique_together": {("dependency", "major_version", "minor_version")}, }, ), migrations.CreateModel( @@ -89,13 +103,6 @@ class Migration(migrations.Migration): ), ("patch_version", models.IntegerField()), ("release_date", models.DateField(blank=True, null=True)), - ( - "release_version", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="outdated.releaseversion", - ), - ), ], options={ "ordering": [ @@ -105,11 +112,10 @@ class Migration(migrations.Migration): "release_version__minor_version", "patch_version", ], - "unique_together": {("release_version", "patch_version")}, }, ), migrations.CreateModel( - name="Project", + name="Dependency", fields=[ ( "id", @@ -120,25 +126,17 @@ class Migration(migrations.Migration): serialize=False, ), ), - ("name", models.CharField(db_index=True, max_length=100)), - ("repo", outdated.models.RepositoryURLField(max_length=100)), + ("name", models.CharField(max_length=100)), ( - "repo_type", + "provider", models.CharField( - choices=[ - ("public", "public"), - ("access-token", "access-token"), - ], - max_length=25, + choices=[("PIP", "PIP"), ("NPM", "NPM")], max_length=10 ), ), - ( - "versioned_dependencies", - models.ManyToManyField(blank=True, to="outdated.version"), - ), ], options={ "ordering": ["name", "id"], + "unique_together": {("name", "provider")}, }, ), migrations.CreateModel( @@ -155,11 +153,11 @@ class Migration(migrations.Migration): ), ("is_primary", outdated.models.UniqueBooleanField(default=False)), ( - "project", + "source", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="maintainers", - to="outdated.project", + to="outdated.dependencysource", ), ), ( @@ -184,8 +182,45 @@ class Migration(migrations.Migration): name="unique_project_repo", ), ), + migrations.AddField( + model_name="dependencysource", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sources", + to="outdated.project", + ), + ), + migrations.AddField( + model_name="releaseversion", + name="dependency", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="outdated.dependency" + ), + ), + migrations.AddField( + model_name="version", + name="release_version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="outdated.releaseversion", + ), + ), + migrations.AddField( + model_name="dependencysource", + name="versions", + field=models.ManyToManyField(blank=True, to="outdated.version"), + ), migrations.AlterUniqueTogether( name="maintainer", - unique_together={("user", "project")}, + unique_together={("user", "source")}, + ), + migrations.AlterUniqueTogether( + name="releaseversion", + unique_together={("dependency", "major_version", "minor_version")}, + ), + migrations.AlterUniqueTogether( + name="version", + unique_together={("release_version", "patch_version")}, ), ] diff --git a/api/outdated/outdated/models.py b/api/outdated/outdated/models.py index c954ca97..9dd9ed0a 100644 --- a/api/outdated/outdated/models.py +++ b/api/outdated/outdated/models.py @@ -124,8 +124,6 @@ def version(self) -> str: class Project(UUIDModel): name = models.CharField(max_length=100, db_index=True) - - versioned_dependencies = models.ManyToManyField(Version, blank=True) repo = RepositoryURLField(max_length=100) repo_type = models.CharField(max_length=25, choices=REPO_TYPES) @@ -208,21 +206,39 @@ class Meta: @property def status(self) -> str: - first = self.versioned_dependencies.first() - return first.release_version.status if first else STATUS_OPTIONS["undefined"] + if not (first := self.sources.first()): + return "UNDEFINED" + return ( + first.versions.first().release_version.status + if first.versions.first() + else STATUS_OPTIONS["undefined"] + ) def __str__(self): return self.name +class DependencySource(UUIDModel): + path = models.CharField() + project = models.ForeignKey( + Project, on_delete=models.CASCADE, related_name="sources" + ) + versions = models.ManyToManyField(Version, blank=True) + + @property + def status(self) -> str: + first = self.versions.first() + return first.release_version.status if first else STATUS_OPTIONS["undefined"] + + class Maintainer(UUIDModel): user = models.ForeignKey(User, on_delete=models.CASCADE) - project = models.ForeignKey( - Project, + source = models.ForeignKey( + DependencySource, on_delete=models.CASCADE, related_name="maintainers", ) - is_primary = UniqueBooleanField(default=False, together=["project"]) + is_primary = UniqueBooleanField(default=False, together=["source"]) class Meta: - unique_together = ("user", "project") + unique_together = ("user", "source") diff --git a/api/outdated/outdated/parser.py b/api/outdated/outdated/parser.py index 878c7f67..c310379f 100644 --- a/api/outdated/outdated/parser.py +++ b/api/outdated/outdated/parser.py @@ -21,7 +21,8 @@ class LockfileParser: """Parse a lockfile and return a list of dependencies.""" - def __init__(self, lockfiles: list[Path]) -> None: + def __init__(self, project: models.Project, lockfiles: list[Path]) -> None: + self.project = project self.lockfiles = lockfiles def _get_provider(self, name: str) -> str: @@ -135,10 +136,8 @@ def _get_release_date(self, version: models.Version) -> date: return parse_date(release_date).date() - def parse(self) -> list[models.Version]: - """Parse the lockfile and return a dictionary of dependencies.""" - versions = [] - + def parse(self) -> None: + """Parse the lockfile and create the DependencySources.""" for lockfile in self.lockfiles: name = lockfile.name data = lockfile.read_text() @@ -173,8 +172,9 @@ def parse(self) -> list[models.Version]: and requirements[0][0] in settings.TRACKED_DEPENDENCIES ] - versions.extend( - self._get_version(dependency, provider) for dependency in dependencies + source, _ = models.DependencySource.objects.get_or_create( + path=name, project=self.project + ) + source.versions.set( + [self._get_version(dependency, provider) for dependency in dependencies] ) - - return versions diff --git a/api/outdated/outdated/serializers.py b/api/outdated/outdated/serializers.py index 24046ded..87d44ae6 100644 --- a/api/outdated/outdated/serializers.py +++ b/api/outdated/outdated/serializers.py @@ -49,7 +49,7 @@ class Meta: class MaintainerSerializer(serializers.ModelSerializer): included_serializers = { "user": "outdated.user.serializers.UserSerializer", - "project": "outdated.outdated.serializers.ProjectSerializer", + "source": "outdated.outdated.serializers.DependencySourceSerializer", } class Meta: @@ -57,11 +57,27 @@ class Meta: fields = "__all__" -class ProjectSerializer(serializers.ModelSerializer): +class DependencySourceSerializer(serializers.ModelSerializer): maintainers = serializers.ResourceRelatedField( many=True, read_only=True, - required=False, + ) + + included_serializers = { + "project": "outdated.outdated.serializers.ProjectSerializer", + "versions": VersionSerializer, + "maintainers": MaintainerSerializer, + } + + class Meta: + model = models.DependencySource + fields = "__all__" + + +class ProjectSerializer(serializers.ModelSerializer): + sources = serializers.ResourceRelatedField( + many=True, + read_only=True, ) access_token = serializers.CharField( @@ -83,8 +99,7 @@ class ProjectSerializer(serializers.ModelSerializer): ) included_serializers = { - "versioned_dependencies": "outdated.outdated.serializers.VersionSerializer", - "maintainers": "outdated.outdated.serializers.MaintainerSerializer", + "sources": DependencySourceSerializer, } class Meta: @@ -100,8 +115,7 @@ class Meta: "repo_type", "access_token", "status", - "versioned_dependencies", - "maintainers", + "sources", ) def create(self, validated_data: dict) -> models.Project: diff --git a/api/outdated/outdated/tests/test_api.py b/api/outdated/outdated/tests/test_api.py index 24a20d53..8fda36b1 100644 --- a/api/outdated/outdated/tests/test_api.py +++ b/api/outdated/outdated/tests/test_api.py @@ -120,10 +120,14 @@ def test_version(client, version_factory): @pytest.mark.parametrize("defined", [True, False]) -def test_project(client, project_factory, version_factory, defined): - generated_project = project_factory( - versioned_dependencies=[version_factory()] if defined else [], +def test_project( + client, project_factory, dependency_source_factory, version_factory, defined +): + generated_project = project_factory() + dependency_source_factory( + versions=[version_factory()] if defined else [], project=generated_project ) + url = reverse("project-list") resp = client.get(url) assert resp.status_code == http_status.HTTP_200_OK @@ -152,20 +156,20 @@ def test_project(client, project_factory, version_factory, defined): ) if defined: for gen_dep_version, resp_dep_version in zip( - resp_detailed.json()["data"]["relationships"]["versioned-dependencies"][ - "data" - ], - generated_project.versioned_dependencies.all(), + resp_detailed.json()["data"]["relationships"]["sources"]["data"], + generated_project.sources.all(), ): assert gen_dep_version["id"] == str(resp_dep_version.id) else: - assert not generated_project.versioned_dependencies.first() + assert not generated_project.sources.first().versions.first() + assert generated_project.sources.first().status == "UNDEFINED" assert generated_project.status == "UNDEFINED" def test_project_ordered_by_eol( client, project_factory, + dependency_source_factory, release_version_factory, version_factory, ): @@ -179,26 +183,23 @@ def test_project_ordered_by_eol( release_version=release_version_factory(up_to_date=True), ) - project_last = project_factory( - name="A project", - versioned_dependencies=[up_to_date_version], - ) - project_middle = project_factory( - name="B project", - versioned_dependencies=[warning_version], - ) - project_first = project_factory( - name="C project", - versioned_dependencies=[outdated_version], - ) + project_up_to_date = project_factory(name="A project") + dependency_source_factory(versions=[up_to_date_version], project=project_up_to_date) + project_warning = project_factory(name="B project") + dependency_source_factory(versions=[warning_version], project=project_warning) + project_outdated = project_factory(name="C project") + dependency_source_factory(versions=[outdated_version], project=project_outdated) + project_undefined = project_factory(name="D project") url = reverse("project-list") resp = client.get(url) + assert resp.status_code == http_status.HTTP_200_OK json = resp.json() - assert json["data"][0]["id"] == str(project_first.pk) - assert json["data"][1]["id"] == str(project_middle.pk) - assert json["data"][2]["id"] == str(project_last.pk) + assert json["data"][0]["id"] == str(project_outdated.pk) + assert json["data"][1]["id"] == str(project_warning.pk) + assert json["data"][2]["id"] == str(project_up_to_date.pk) + assert json["data"][3]["id"] == str(project_undefined.pk) def test_maintainer(client, maintainer): @@ -216,15 +217,15 @@ def test_maintainer(client, maintainer): == str(maintainer.user.id) ) assert ( - relationships["project"]["data"]["id"] - == detailed_relationships["project"]["data"]["id"] - == str(maintainer.project.id) + relationships["source"]["data"]["id"] + == detailed_relationships["source"]["data"]["id"] + == str(maintainer.source.id) ) assert ( resp.json()["data"][0]["attributes"] == resp_detailed.json()["data"]["attributes"] ) - assert maintainer.project.maintainers.all()[0] == maintainer + assert maintainer.source.maintainers.all()[0] == maintainer @pytest.mark.django_db(transaction=True) diff --git a/api/outdated/outdated/tests/test_parser.py b/api/outdated/outdated/tests/test_parser.py index e6eb4402..c017c6f9 100644 --- a/api/outdated/outdated/tests/test_parser.py +++ b/api/outdated/outdated/tests/test_parser.py @@ -7,6 +7,7 @@ import pytest from requests import exceptions +from outdated.outdated.models import Version from outdated.outdated.parser import LockfileParser from outdated.outdated.tracking import Tracker @@ -127,12 +128,12 @@ def test_parser(db, tmp_repo_root, project, lockfile, content, expected): assert len(lockfiles) == 1 assert lockfiles[0].name == lockfile - results = LockfileParser(lockfiles).parse() + LockfileParser(project, lockfiles).parse() - assert len(results) == len(expected) + assert len(project.sources.values_list("versions", flat=True)) == len(expected) - for result in results: - assert str(result) in expected + for result in project.sources.values_list("versions", flat=True): + assert str(Version.objects.get(id=result)) in expected @pytest.mark.django_db() @@ -169,6 +170,7 @@ def test_fetch_end_of_life( associations: dict, call_count: int, requests_mock: Mocker, + project: models.Project, ) -> None: settings.ENDOFLIFE_DATE_ASSOCIATIONS = associations mocker.patch.object( @@ -188,7 +190,9 @@ def test_fetch_end_of_life( }, ) for dependency_name in dependency_names: - LockfileParser([])._get_version((dependency_name, "4.4.1"), provider="PIP") # noqa: SLF001 + LockfileParser(project, [])._get_version( # noqa: SLF001 + (dependency_name, "4.4.1"), provider="PIP" + ) assert get_end_of_life_date_spy.call_count == call_count @@ -196,16 +200,16 @@ def test_fetch_end_of_life( @pytest.mark.parametrize( "exception", [exceptions.ConnectTimeout, exceptions.ReadTimeout] ) -def test_fetch_end_of_life_ignore_timeout(mocker, requests_mock, exception): +def test_fetch_end_of_life_ignore_timeout(mocker, requests_mock, exception, project): mocker.patch.object( LockfileParser, "_get_release_date", return_value=date(2024, 1, 1) ) requests_mock.get(compile("https://endoflife.date/api/[-a-z]+/4.4"), exc=exception) - LockfileParser([])._get_version(("django", "4.4.1"), provider="PIP") # noqa: SLF001 + LockfileParser(project, [])._get_version(("django", "4.4.1"), provider="PIP") # noqa: SLF001 @pytest.mark.django_db() -def test_fetch_end_of_life_invalid_json(mocker, requests_mock): +def test_fetch_end_of_life_invalid_json(mocker, requests_mock, project): mocker.patch.object( LockfileParser, "_get_release_date", return_value=date(2024, 1, 1) ) @@ -214,7 +218,7 @@ def test_fetch_end_of_life_invalid_json(mocker, requests_mock): text="503 Service Temporarily Unavailable", status_code=503, ) - LockfileParser([])._get_version(("django", "4.4.1"), provider="PIP") # noqa: SLF001 + LockfileParser(project, [])._get_version(("django", "4.4.1"), provider="PIP") # noqa: SLF001 @pytest.mark.django_db() @@ -223,6 +227,7 @@ def test_fetch_end_of_life_no_overwrite_if_already_set( dependency_factory: DependencyFactory, release_version_factory: ReleaseVersionFactory, version_factory: VersionFactory, + project: models.Project, ) -> None: end_of_life = date(2025, 1, 1) @@ -244,7 +249,7 @@ def test_fetch_end_of_life_no_overwrite_if_already_set( version: models.Version = version_factory(release_version=release_version) - LockfileParser([])._get_version( # noqa: SLF001 + LockfileParser(project, [])._get_version( # noqa: SLF001 (dependency.name, version.version), provider=dependency.provider ) diff --git a/api/outdated/outdated/tests/test_tracking.py b/api/outdated/outdated/tests/test_tracking.py index 1ab25870..8b6c2c6c 100644 --- a/api/outdated/outdated/tests/test_tracking.py +++ b/api/outdated/outdated/tests/test_tracking.py @@ -10,7 +10,7 @@ from rest_framework import status from outdated.outdated import tracking -from outdated.outdated.models import Project +from outdated.outdated.models import Project, Version from outdated.outdated.parser import LockfileParser from outdated.outdated.tracking import RepoError, Tracker @@ -217,6 +217,7 @@ def test_sync( tracker_mock, exists, mocker, + dependency_source_factory, version_factory, ): project_path = tmp_repo_root / project.clone_path @@ -237,15 +238,17 @@ def test_sync( ) versions = version_factory.create_batch(5) + + def side_effect() -> None: + dependency_source_factory(versions=versions, project=project) + lockfile_parser_parser_mock = mocker.patch.object( - LockfileParser, - "parse", - return_value=versions, + LockfileParser, "parse", side_effect=side_effect ) tracker = Tracker(project) assert tracker.local_path == project_path - assert not project.versioned_dependencies.all() + assert not project.sources.all() if exists: project_path.mkdir(parents=True, exist_ok=False) @@ -258,13 +261,15 @@ def test_sync( tracker_fetch_mock.assert_called_once() - lockfile_parser_init_mock.assert_called_once_with([]) + lockfile_parser_init_mock.assert_called_once_with(project, []) lockfile_parser_parser_mock.assert_called_once_with() tracker_lockfile_mock.assert_called_once() - assert set(project.versioned_dependencies.all()) == set(versions) + assert set(project.sources.values_list("versions", flat=True)) == { + v.id for v in versions + } @pytest.mark.parametrize("exists", [True, False]) @@ -395,11 +400,12 @@ def test_sync_tracks_changes(tmp_repo_root, project_factory): tracker.checkout() tracker.sync() - assert project.versioned_dependencies.count() == 2 + versions = Version.objects.filter( + id__in=project.sources.values_list("versions", flat=True) + ) + assert len(versions) == 2 for requirement in POETRY_LOCK_REQUIREMENTS: - assert requirement in [ - str(version) for version in project.versioned_dependencies.all() - ] + assert requirement in [str(version) for version in versions] def replace_versions(s: str | list[str]) -> str: if isinstance(s, list): @@ -418,8 +424,10 @@ def replace_versions(s: str | list[str]) -> str: tracker.sync() - assert project.versioned_dependencies.count() == 2 + versions = Version.objects.filter( + id__in=project.sources.values_list("versions", flat=True) + ) + + assert len(versions) == 2 for requirement in replace_versions(POETRY_LOCK_REQUIREMENTS): - assert requirement in [ - str(version) for version in project.versioned_dependencies.all() - ] + assert requirement in [str(version) for version in versions] diff --git a/api/outdated/outdated/tracking.py b/api/outdated/outdated/tracking.py index 6b177f8b..794e0944 100644 --- a/api/outdated/outdated/tracking.py +++ b/api/outdated/outdated/tracking.py @@ -112,8 +112,7 @@ def sync(self): if not self.local_path.exists(): self.clone() self.fetch() - dependencies = LockfileParser(self.lockfiles).parse() - self.project.versioned_dependencies.set(dependencies) + LockfileParser(self.project, self.lockfiles).parse() def setup(self): # pragma: no cover self.clone() diff --git a/api/outdated/outdated/views.py b/api/outdated/outdated/views.py index e0a16588..07eab5e8 100644 --- a/api/outdated/outdated/views.py +++ b/api/outdated/outdated/views.py @@ -15,7 +15,7 @@ class ProjectViewSet(ModelViewSet): queryset = models.Project.objects.annotate( - min_end_of_life=Min("versioned_dependencies__release_version__end_of_life") + min_end_of_life=Min("sources__versions__release_version__end_of_life") ).order_by("min_end_of_life") serializer_class = serializers.ProjectSerializer @@ -62,6 +62,11 @@ class DependencyViewSet(ModelViewSet): permission_classes = [is_readonly()] +class DependencySourceViewSet(ModelViewSet): + queryset = models.DependencySource.objects.all() + serializer_class = serializers.DependencySourceSerializer + + class MaintainerViewset(ModelViewSet): queryset = models.Maintainer.objects.all() serializer_class = serializers.MaintainerSerializer diff --git a/api/outdated/tests/test_unique_boolean_field.py b/api/outdated/tests/test_unique_boolean_field.py index d028fb8d..a7880a9d 100644 --- a/api/outdated/tests/test_unique_boolean_field.py +++ b/api/outdated/tests/test_unique_boolean_field.py @@ -6,7 +6,7 @@ def test_unique_boolean_field(db, maintainer_factory): assert Maintainer.objects.count() == 1 assert maintainer.is_primary - other_maintainer = maintainer_factory(project=maintainer.project) + other_maintainer = maintainer_factory(source=maintainer.source) assert Maintainer.objects.count() == 2 assert not other_maintainer.is_primary diff --git a/api/outdated/urls.py b/api/outdated/urls.py index e9603402..b05479ce 100644 --- a/api/outdated/urls.py +++ b/api/outdated/urls.py @@ -9,6 +9,7 @@ router.register(r"maintainers", views.MaintainerViewset) router.register(r"dependencies", views.DependencyViewSet) router.register(r"release-versions", views.ReleaseVersionViewSet) +router.register(r"dependency-sources", views.DependencySourceViewSet) router.register(r"versions", views.VersionViewSet) router.register(r"users", UserViewSet) diff --git a/ember/app/components/form.gjs b/ember/app/components/form.gjs index 45185456..c14ebb8c 100644 --- a/ember/app/components/form.gjs +++ b/ember/app/components/form.gjs @@ -50,7 +50,12 @@ export default class FormComponent extends Component { }