diff --git a/buildingmotif-app/src/app/app-routing.module.ts b/buildingmotif-app/src/app/app-routing.module.ts index 50a5d8745..a0fe720d7 100644 --- a/buildingmotif-app/src/app/app-routing.module.ts +++ b/buildingmotif-app/src/app/app-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { TemplateSearchComponent } from '../app/template-search/template-search.component' -import { TemplateSearchResolver } from '../app/template-search/template-search.resolver' import { TemplateDetailComponent } from '../app/template-detail/template-detail.component' import { ModelSearchComponent } from '../app/model-search/model-search.component' import { ModelSearchResolver } from '../app/model-search/model-search.resolver' @@ -16,7 +15,7 @@ const routes: Routes = [ { path: 'templates/:id', component: TemplateDetailComponent }, { path: 'templates/:id/evaluate', component: TemplateEvaluateComponent, resolve: {TemplateEvaluateResolver}}, { path: 'models/:id', component: ModelDetailComponent, resolve: {ModelDetailResolver}}, - { path: 'templates', component: TemplateSearchComponent, resolve: {templateSearch:TemplateSearchResolver}}, + { path: 'templates', component: TemplateSearchComponent}, { path: 'models', component: ModelSearchComponent, resolve: {ModelSearchResolver}}, { path: '', redirectTo: '/templates', pathMatch: 'full' }, ]; diff --git a/buildingmotif-app/src/app/app.module.ts b/buildingmotif-app/src/app/app.module.ts index 001f42f72..ce3787937 100644 --- a/buildingmotif-app/src/app/app.module.ts +++ b/buildingmotif-app/src/app/app.module.ts @@ -36,6 +36,8 @@ import { LibraryService } from './library/library.service'; import {MatSidenavModule} from '@angular/material/sidenav'; import {MatTabsModule} from '@angular/material/tabs'; import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatStepperModule} from '@angular/material/stepper'; @NgModule({ declarations: [ @@ -76,6 +78,8 @@ import {MatCheckboxModule} from '@angular/material/checkbox'; MatSidenavModule, MatTabsModule, MatCheckboxModule, + MatDialogModule, + MatStepperModule, ], providers: [TemplateDetailService, LibraryService], bootstrap: [AppComponent] diff --git a/buildingmotif-app/src/app/model-detail/model-detail.component.html b/buildingmotif-app/src/app/model-detail/model-detail.component.html index 9074afa49..578858b3c 100644 --- a/buildingmotif-app/src/app/model-detail/model-detail.component.html +++ b/buildingmotif-app/src/app/model-detail/model-detail.component.html @@ -22,10 +22,11 @@ - Content 1 - Content 2 + + + - + diff --git a/buildingmotif-app/src/app/model-detail/model-detail.component.ts b/buildingmotif-app/src/app/model-detail/model-detail.component.ts index 4217bf957..68161a6cd 100644 --- a/buildingmotif-app/src/app/model-detail/model-detail.component.ts +++ b/buildingmotif-app/src/app/model-detail/model-detail.component.ts @@ -8,6 +8,8 @@ import { MatSnackBarHorizontalPosition, MatSnackBarVerticalPosition, } from '@angular/material/snack-bar'; +import {MatDialog} from '@angular/material/dialog'; +import {TemplateEvaluateComponent} from '../template-evaluate/template-evaluate.component' @Component({ selector: 'app-model-detail', @@ -37,6 +39,7 @@ export class ModelDetailComponent{ private route: ActivatedRoute, private ModelDetailService: ModelDetailService, private _snackBar: MatSnackBar, + public dialog: MatDialog, ) { [this.model, this.graph] = route.snapshot.data["ModelDetailResolver"]; this.graphFormControl.setValue(this.graph); @@ -63,4 +66,11 @@ export class ModelDetailComponent{ undoChangesToGraph(): void { this.graphFormControl.setValue(this.graph) } + + openEvaulateEvent(templateId: number): void { + this.dialog.open( + TemplateEvaluateComponent, + {data: {templateId, modelId: this.model.id}} + ); + } } diff --git a/buildingmotif-app/src/app/model-validate/model-validate.component.ts b/buildingmotif-app/src/app/model-validate/model-validate.component.ts index e3e22fb13..37359e4da 100644 --- a/buildingmotif-app/src/app/model-validate/model-validate.component.ts +++ b/buildingmotif-app/src/app/model-validate/model-validate.component.ts @@ -70,7 +70,7 @@ export class ModelValidateComponent implements OnInit{ const selectedLibraries = this.libraries.filter((_, i) => this.selectedLibrariesForm.value[i]) const args = selectedLibraries.map(l => l.id); - if (!!this.modelId){ + if (this.modelId !== undefined){ this.showValidatingSpinner = true; this.modelValidateService.validateModel(this.modelId, args).subscribe( diff --git a/buildingmotif-app/src/app/template-evaluate/template-evaluate-form/template-evaluate-form.component.html b/buildingmotif-app/src/app/template-evaluate/template-evaluate-form/template-evaluate-form.component.html index 8193c6533..107e9505f 100644 --- a/buildingmotif-app/src/app/template-evaluate/template-evaluate-form/template-evaluate-form.component.html +++ b/buildingmotif-app/src/app/template-evaluate/template-evaluate-form/template-evaluate-form.component.html @@ -25,6 +25,7 @@ mat-raised-button color="primary" (click)="evaluateClicked()" - [disabled]="parametersForm.invalid">Evaluate + [disabled]="parametersForm.invalid" + matStepperNext>Evaluate \ No newline at end of file diff --git a/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.css b/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.css index 8c5c76520..74c56145d 100644 --- a/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.css +++ b/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.css @@ -1,5 +1,7 @@ -.addToModel{ +.actions{ display: flex; gap: 1rem; + padding-top: 1rem; + justify-content:flex-end; align-items: baseline; } \ No newline at end of file diff --git a/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.html b/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.html index 66b9d88f1..52100ee3e 100644 --- a/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.html +++ b/buildingmotif-app/src/app/template-evaluate/template-evaluate-result/template-evaluate-result.component.html @@ -1,23 +1,19 @@ + + -
-
- - - - {{model.name}} - - - -
- +
+ - - - - -
\ No newline at end of file + +
+ +
+ + +
no templates
+ + +
+ + + {{template.name}} + + + + + + +
+
diff --git a/buildingmotif-app/src/app/template-search/template-search.component.ts b/buildingmotif-app/src/app/template-search/template-search.component.ts index 0a556e209..6491aa412 100644 --- a/buildingmotif-app/src/app/template-search/template-search.component.ts +++ b/buildingmotif-app/src/app/template-search/template-search.component.ts @@ -1,9 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter} from '@angular/core'; import { TemplateSearchService, Template } from './template-search.service'; import {FormControl} from '@angular/forms'; import {Observable} from 'rxjs'; import {map, startWith} from 'rxjs/operators'; -import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-template-search', @@ -12,24 +11,40 @@ import { ActivatedRoute } from '@angular/router'; providers: [TemplateSearchService], }) export class TemplateSearchComponent implements OnInit{ - templates: Template[] = []; + error: any = undefined; + templates: Template[] | undefined = undefined; fitlerStringControl = new FormControl(''); filteredTemplates: Observable = new Observable(); - constructor(private route: ActivatedRoute) { - this.templates = this.route.snapshot.data["templateSearch"]; - } + // When in model detail page, allow init of evaluation. + @Input() evaulateModelId: number | undefined; + @Output() openEvaulateEvent = new EventEmitter(); - private _filterTemplatesByName(value: string): Template[] { - const filterValue = value.toLowerCase(); + constructor(private TemplateSearchService: TemplateSearchService){} - return this.templates.filter(template => template.name.toLocaleLowerCase().includes(filterValue)) + openEvaluateTemplate(template_id: number): void { + this.openEvaulateEvent.emit(template_id); } - ngOnInit() { + private _setTemplates(data: Template[]): void { + this.templates = data; this.filteredTemplates = this.fitlerStringControl.valueChanges.pipe( startWith(''), map(value => this._filterTemplatesByName(value || '')), ); } + + private _filterTemplatesByName(value: string): Template[] { + const filterValue = value.toLowerCase(); + if(this.templates == undefined) return []; + return this.templates.filter(template => template.name.toLocaleLowerCase().includes(filterValue)) + } + + ngOnInit() { + this.TemplateSearchService.getAllTemplates() + .subscribe({ + next: (data: Template[]) => this._setTemplates(data), // success path + error: (error) => this.error = error // error path + }); + } } diff --git a/buildingmotif-app/src/app/template-search/template-search.resolver.ts b/buildingmotif-app/src/app/template-search/template-search.resolver.ts deleted file mode 100644 index cd9a2d47b..000000000 --- a/buildingmotif-app/src/app/template-search/template-search.resolver.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - Router, Resolve, - RouterStateSnapshot, - ActivatedRouteSnapshot -} from '@angular/router'; -import { Observable, of } from 'rxjs'; -import { TemplateSearchService, Template } from './template-search.service'; - -@Injectable({ - providedIn: 'root' -}) -export class TemplateSearchResolver implements Resolve { - - constructor(private templateSearchService: TemplateSearchService) {} - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.templateSearchService.getAllTemplates() - } -} diff --git a/buildingmotif/api/views/template.py b/buildingmotif/api/views/template.py index 903cb372d..86350aa83 100644 --- a/buildingmotif/api/views/template.py +++ b/buildingmotif/api/views/template.py @@ -8,7 +8,7 @@ from sqlalchemy.orm.exc import NoResultFound from buildingmotif.api.serializers.template import serialize -from buildingmotif.dataclasses import Template +from buildingmotif.dataclasses import Model, Template blueprint = Blueprint("templates", __name__) @@ -69,8 +69,21 @@ def evaluate(template_id: int) -> flask.Response: "message": "request content type must be json" }, status.HTTP_400_BAD_REQUEST + model_id = request.get_json().get("model_id") + if model_id is None: + return {"message": "body must contain 'model_id'"}, status.HTTP_400_BAD_REQUEST + try: + model = Model.load(model_id) + except NoResultFound: + return {"message": f"No model with id {model_id}"}, status.HTTP_404_NOT_FOUND + + bindings = request.get_json().get("bindings") + if bindings is None: + return {"message": "body must contain 'bindings'"}, status.HTTP_400_BAD_REQUEST + bindings = get_bindings(bindings) + bindings = {k: model.name.rstrip("/") + "/" + v for k, v in bindings.items()} + # parse bindings from input JSON - bindings = get_bindings(request.get_json()) graph_or_template = template.evaluate(bindings=bindings) if isinstance(graph_or_template, Template): graph = graph_or_template.body diff --git a/tests/unit/api/test_template.py b/tests/unit/api/test_template.py index 254ed7bb7..fa6e9cb2f 100644 --- a/tests/unit/api/test_template.py +++ b/tests/unit/api/test_template.py @@ -1,7 +1,7 @@ from flask_api import status from rdflib import Graph, Namespace -from buildingmotif.dataclasses import Library +from buildingmotif.dataclasses import Library, Model from buildingmotif.namespaces import BRICK, A BLDG = Namespace("urn:building/") @@ -78,6 +78,7 @@ def test_get_template_not_found(client): def test_evaluate(client, building_motif): + model = Model.create(name="urn:my_model") lib = Library.load(directory="tests/unit/fixtures/templates") zone = lib.get_template_by_name("zone") zone.inline_dependencies() @@ -85,12 +86,89 @@ def test_evaluate(client, building_motif): results = client.post( f"/templates/{zone.id}/evaluate", - json={"name": {"@id": BLDG["zone1"]}, "cav": {"@id": BLDG["cav1"]}}, + json={ + "model_id": model.id, + "bindings": {"name": {"@id": BLDG["zone1"]}, "cav": {"@id": BLDG["cav1"]}}, + }, ) assert results.status_code == status.HTTP_200_OK graph = Graph().parse(data=results.data, format="ttl") - assert (BLDG["cav1"], A, BRICK.CAV) in graph - assert (BLDG["zone1"], A, BRICK.HVAC_Zone) in graph - assert (BLDG["zone1"], BRICK.isFedBy, BLDG["cav1"]) in graph + assert (model.name + "/" + BLDG["cav1"], A, BRICK.CAV) in graph + assert (model.name + "/" + BLDG["zone1"], A, BRICK.HVAC_Zone) in graph + assert ( + model.name + "/" + BLDG["zone1"], + BRICK.isFedBy, + model.name + "/" + BLDG["cav1"], + ) in graph assert len(list(graph.triples((None, None, None)))) == 3 + + +def test_evaluate_bad_templated_id(client, building_motif): + model = Model.create(name="urn:my_model") + + results = client.post( + "/templates/-1/evaluate", + json={ + "model_id": model.id, + "bindings": {"name": {"@id": BLDG["zone1"]}, "cav": {"@id": BLDG["cav1"]}}, + }, + ) + + assert results.status_code == 404 + + +def test_evaluate_no_body(client, building_motif): + lib = Library.load(directory="tests/unit/fixtures/templates") + zone = lib.get_template_by_name("zone") + zone.inline_dependencies() + assert zone.parameters == {"name", "cav"} + + results = client.post(f"/templates/{zone.id}/evaluate") + + assert results.status_code == 400 + + +def test_evaluate_bad_body(client, building_motif): + model = Model.create(name="urn:my_model") + lib = Library.load(directory="tests/unit/fixtures/templates") + zone = lib.get_template_by_name("zone") + zone.inline_dependencies() + assert zone.parameters == {"name", "cav"} + + results = client.post( + f"/templates/{zone.id}/evaluate", + json={ + # no model + "bindings": {"name": {"@id": BLDG["zone1"]}, "cav": {"@id": BLDG["cav1"]}}, + }, + ) + + assert results.status_code == 400 + + results = client.post( + f"/templates/{zone.id}/evaluate", + json={ + "model_id": model.id, + # no bindings + }, + ) + + assert results.status_code == 400 + + +def test_evaluate_bad_model_id(client, building_motif): + lib = Library.load(directory="tests/unit/fixtures/templates") + zone = lib.get_template_by_name("zone") + zone.inline_dependencies() + assert zone.parameters == {"name", "cav"} + + results = client.post( + f"/templates/{zone.id}/evaluate", + json={ + "model_id": -1, + "bindings": {"name": {"@id": BLDG["zone1"]}, "cav": {"@id": BLDG["cav1"]}}, + }, + ) + + assert results.status_code == 404