Skip to content

Commit

Permalink
Merge pull request #1063 from DSD-DBS/feat-rename-toolmodels
Browse files Browse the repository at this point in the history
feat: Add possibility to update toolmodel name
  • Loading branch information
MoritzWeber0 authored Oct 23, 2023
2 parents 404b23c + 5526048 commit 66fa060
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 10 deletions.
4 changes: 4 additions & 0 deletions backend/capellacollab/projects/toolmodels/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,17 @@ def update_model(
db: orm.Session,
model: models.DatabaseCapellaModel,
description: str | None,
name: str | None,
version: tools_models.DatabaseVersion,
nature: tools_models.DatabaseNature,
) -> models.DatabaseCapellaModel:
model.version = version
model.nature = nature
if description:
model.description = description
if name:
model.name = name
model.slug = slugify.slugify(name)
db.commit()
return model

Expand Down
1 change: 1 addition & 0 deletions backend/capellacollab/projects/toolmodels/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class PostCapellaModel(pydantic.BaseModel):


class PatchCapellaModel(pydantic.BaseModel):
name: str | None = None
description: str | None = None
version_id: int
nature_id: int
Expand Down
21 changes: 20 additions & 1 deletion backend/capellacollab/projects/toolmodels/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid

import fastapi
import slugify
from fastapi import status
from sqlalchemy import exc, orm

Expand Down Expand Up @@ -121,11 +122,27 @@ def create_new_tool_model(
)
def patch_tool_model(
body: models.PatchCapellaModel,
project: projects_models.DatabaseProject = fastapi.Depends(
projects_injectables.get_existing_project
),
model: models.DatabaseCapellaModel = fastapi.Depends(
injectables.get_existing_capella_model
),
db: orm.Session = fastapi.Depends(database.get_db),
) -> models.DatabaseCapellaModel:
if body.name:
new_slug = slugify.slugify(body.name)

if model.slug != new_slug and crud.get_model_by_slugs(
db, project.slug, new_slug
):
raise fastapi.HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={
"reason": "A model with a similar name already exists."
},
)

version = get_version_by_id_or_raise(db, body.version_id)
if version.tool != model.tool:
raise fastapi.HTTPException(
Expand All @@ -144,7 +161,9 @@ def patch_tool_model(
},
)

return crud.update_model(db, model, body.description, version, nature)
return crud.update_model(
db, model, body.description, body.name, version, nature
)


@router.delete(
Expand Down
88 changes: 88 additions & 0 deletions backend/tests/projects/toolmodels/test_toolmodel_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0


from unittest import mock

import pytest
from fastapi import testclient
from sqlalchemy import orm

from capellacollab.__main__ import app
from capellacollab.projects import injectables as projects_injectables
from capellacollab.projects import models as projects_models
from capellacollab.projects.toolmodels import (
injectables as toolmodels_injectables,
)
from capellacollab.projects.toolmodels import models as toolmodels_models
from capellacollab.users import crud as users_crud
from capellacollab.users import models as users_models


@pytest.fixture(name="override_dependency")
def fixture_override_dependency():
mock_project = mock.Mock(name="DatabaseProject")

mock_model = mock.Mock(name="DatabaseModel")
mock_model.slug = "any-slug"
mock_model.tool = mock.Mock(name="tool")

app.dependency_overrides[
projects_injectables.get_existing_project
] = lambda: mock_project
app.dependency_overrides[
toolmodels_injectables.get_existing_capella_model
] = lambda: mock_model

yield

del app.dependency_overrides[projects_injectables.get_existing_project]
del app.dependency_overrides[
toolmodels_injectables.get_existing_capella_model
]


def test_rename_toolmodel_successful(
capella_model: toolmodels_models.DatabaseCapellaModel,
project: projects_models.DatabaseProject,
client: testclient.TestClient,
executor_name: str,
db: orm.Session,
):
users_crud.create_user(db, executor_name, users_models.Role.ADMIN)

response = client.patch(
f"/api/v1/projects/{project.slug}/models/{capella_model.slug}",
json={
"name": "new-name",
"version_id": capella_model.tool.versions[0].id,
"nature_id": capella_model.tool.natures[0].id,
},
)

assert response.status_code == 200
assert "new-name" in response.text


@pytest.mark.usefixtures("override_dependency")
def test_rename_toolmodel_where_name_already_exists(
client: testclient.TestClient,
executor_name: str,
db: orm.Session,
):
users_crud.create_user(db, executor_name, users_models.Role.ADMIN)

with mock.patch(
"capellacollab.projects.toolmodels.crud.get_model_by_slugs",
autospec=True,
) as mock_get_model_by_slugs:
mock_get_model_by_slugs.return_value = "anything"

response = client.patch(
"/api/v1/projects/any/models/any",
json={"name": "new-name", "version_id": -1, "nature_id": -1},
)

assert response.status_code == 409
assert "A model with a similar name already exists" in response.text
mock_get_model_by_slugs.assert_called_once()
5 changes: 4 additions & 1 deletion frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import slugify from 'slugify';
import { NavBarService } from 'src/app/general/nav-bar/nav-bar.service';
import { PageLayoutService } from './page-layout/page-layout.service';
import { FullscreenService } from './sessions/service/fullscreen.service';
Expand All @@ -19,7 +20,9 @@ export class AppComponent implements AfterViewInit {
public pageLayoutService: PageLayoutService,
public fullscreenService: FullscreenService,
private navBarService: NavBarService,
) {}
) {
slugify.extend({ '.': '-' });
}

@ViewChild('sidenav') private sidenav?: MatSidenav;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
<mat-card *ngIf="modelService.model$ | async" class="grow basis-1">
<mat-card-title>{{ (modelService.model$ | async)!.name }}</mat-card-title>
<form (submit)="onSubmit()" [formGroup]="form">
<fieldset>
<mat-form-field appearance="fill">
<mat-label>Name</mat-label>
<input matInput formControlName="name" />

<mat-error *ngIf="form.controls.name.errors?.uniqueSlug"
>A model with a similar name already exists.</mat-error
>
</mat-form-field>
</fieldset>

<fieldset>
<mat-form-field appearance="fill">
<mat-label>Description</mat-label>
Expand Down Expand Up @@ -62,7 +73,14 @@
Delete
</button>
</span>
<button mat-flat-button color="primary" type="submit">Submit</button>
<button
mat-flat-button
color="primary"
type="submit"
[disabled]="!form.valid"
>
Submit
</button>
</div>
</form>
</mat-card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ProjectService } from '../../service/project.service';
})
export class ModelDescriptionComponent implements OnInit {
form = new FormGroup({
name: new FormControl<string>(''),
description: new FormControl<string>(''),
nature: new FormControl<number>(-1),
version: new FormControl<number>(-1),
Expand Down Expand Up @@ -57,7 +58,12 @@ export class ModelDescriptionComponent implements OnInit {
model.git_models.length || model.t4c_models.length
);

this.form.controls.name.setAsyncValidators(
this.modelService.asyncSlugValidator(model),
);

this.form.patchValue({
name: model.name,
description: model.description,
nature: model.nature?.id,
version: model.version?.id,
Expand All @@ -83,14 +89,21 @@ export class ModelDescriptionComponent implements OnInit {
onSubmit(): void {
if (this.form.value && this.modelSlug && this.projectSlug) {
this.modelService
.updateModelDescription(this.projectSlug!, this.modelSlug!, {
.updateModelDescription(this.projectSlug, this.modelSlug, {
name: this.form.value.name || undefined,
description: this.form.value.description || '',
nature_id: this.form.value.nature || undefined,
version_id: this.form.value.version || undefined,
})
.subscribe(() =>
this.router.navigate(['../../..'], { relativeTo: this.route }),
);
.subscribe({
next: () => {
this.toastService.showSuccess(
'Model updated',
`${this.modelSlug} has been updated`,
);
this.router.navigate(['../../..'], { relativeTo: this.route });
},
});
}
}

Expand Down
9 changes: 6 additions & 3 deletions frontend/src/app/projects/models/service/model.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ export class ModelService {
this.loadModels(projectSlug);
this._model.next(model);
},
error: () => this._model.next(undefined),
}),
);
}
Expand All @@ -132,13 +131,16 @@ export class ModelService {
this._models.next(undefined);
}

asyncSlugValidator(): AsyncValidatorFn {
asyncSlugValidator(ignoreModel?: Model): AsyncValidatorFn {
const ignoreSlug = !!ignoreModel ? ignoreModel.slug : -1;
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const modelSlug = slugify(control.value, { lower: true });
return this.models$.pipe(
take(1),
map((models) => {
return models?.find((model) => model.slug === modelSlug)
return models?.find(
(model) => model.slug === modelSlug && model.slug !== ignoreSlug,
)
? { uniqueSlug: { value: modelSlug } }
: null;
}),
Expand Down Expand Up @@ -168,6 +170,7 @@ export type Model = {
};

export type PatchModel = {
name?: string;
description?: string;
nature_id?: number;
version_id?: number;
Expand Down

0 comments on commit 66fa060

Please sign in to comment.