Merge branch 'master' into large-files

# Conflicts:
#	routes/file/file_methods.go
This commit is contained in:
Sonja Happ 2021-01-13 12:09:23 +01:00
commit b9483e48d0
26 changed files with 2912 additions and 94 deletions

View file

@ -22,11 +22,10 @@ stages:
- test
- deploy
# Stage: build
##############################################################################
build:backend:
build:
stage: build
image: ${GO_IMAGE}
script:
@ -42,7 +41,7 @@ build:backend:
# Stage: test
##############################################################################
test:gotest:
test:
stage: test
image: ${GO_IMAGE}
variables:
@ -62,75 +61,13 @@ test:gotest:
-coverprofile ./testcover.txt
- go tool cover -func=testcover.txt
dependencies:
- build:backend
test:database:
stage: test
image: ${GO_IMAGE}
variables:
TEST_FOLDER: database
MODE: test
DB_NAME: ${POSTGRES_DB}
DB_HOST: ${POSTGRES_HOST}
DB_USER: ${POSTGRES_USER}
DB_PASS: ${POSTGRES_PASSWORD}
script:
- go mod tidy
- cd ${TEST_FOLDER}
- go test -v
dependencies:
- build:backend
test:scenario:
extends: test:database
variables:
TEST_FOLDER: routes/scenario
test:component-configuration:
extends: test:database
variables:
TEST_FOLDER: routes/component-configuration
test:signal:
extends: test:database
variables:
TEST_FOLDER: routes/signal
test:dashboard:
extends: test:database
variables:
TEST_FOLDER: routes/dashboard
test:widget:
extends: test:database
variables:
TEST_FOLDER: routes/widget
test:infrastructure-component:
extends: test:database
variables:
TEST_FOLDER: routes/infrastructure-component
test:file:
extends: test:database
variables:
TEST_FOLDER: routes/file
test:user:
extends: test:database
variables:
TEST_FOLDER: routes/user
test:healthz:
extends: test:database
variables:
TEST_FOLDER: routes/healthz
- build
# Stage: deploy
##############################################################################
deploy:image:
deploy:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:debug
@ -146,4 +83,4 @@ deploy:image:
--cache=true
--cache-ttl=12h
dependencies:
- test:gotest
- test

View file

@ -93,6 +93,7 @@ func DropTables() {
DBpool.DropTableIfExists(&User{})
DBpool.DropTableIfExists(&Dashboard{})
DBpool.DropTableIfExists(&Widget{})
DBpool.DropTableIfExists(&Result{})
// The following statement deletes the many to many relationship between users and scenarios
DBpool.DropTableIfExists("user_scenarios")
}
@ -107,4 +108,5 @@ func MigrateModels() {
DBpool.AutoMigrate(&User{})
DBpool.AutoMigrate(&Dashboard{})
DBpool.AutoMigrate(&Widget{})
DBpool.AutoMigrate(&Result{})
}

View file

@ -144,6 +144,8 @@ func TestScenarioAssociations(t *testing.T) {
dashboardB := Dashboard{}
fileA := File{}
fileB := File{}
resultA := Result{}
resultB := Result{}
// add scenarios to DB
assert.NoError(t, DBpool.Create(&scenarioA).Error)
@ -165,6 +167,10 @@ func TestScenarioAssociations(t *testing.T) {
assert.NoError(t, DBpool.Create(&fileA).Error)
assert.NoError(t, DBpool.Create(&fileB).Error)
// add results to DB
assert.NoError(t, DBpool.Create(&resultA).Error)
assert.NoError(t, DBpool.Create(&resultB).Error)
// add many-to-many associations between users and scenarios
// User HM Scenarios, Scenario HM Users (Many-to-Many)
assert.NoError(t, DBpool.Model(&scenarioA).Association("Users").Append(&userA).Error)
@ -184,6 +190,10 @@ func TestScenarioAssociations(t *testing.T) {
assert.NoError(t, DBpool.Model(&scenarioA).Association("Dashboards").Append(&dashboardA).Error)
assert.NoError(t, DBpool.Model(&scenarioA).Association("Dashboards").Append(&dashboardB).Error)
// Scenario HM Results
assert.NoError(t, DBpool.Model(&scenarioA).Association("Results").Append(&resultA).Error)
assert.NoError(t, DBpool.Model(&scenarioA).Association("Results").Append(&resultB).Error)
var scenario1 Scenario
assert.NoError(t, DBpool.Find(&scenario1, 1).Error, fmt.Sprintf("Find Scenario with ID=1"))
@ -218,6 +228,14 @@ func TestScenarioAssociations(t *testing.T) {
assert.Fail(t, "Scenario Associations",
"Expected to have %v Files. Has %v.", 2, len(files))
}
// Get results of scenario1
var results []File
assert.NoError(t, DBpool.Model(&scenario1).Related(&results, "Results").Error)
if len(files) != 2 {
assert.Fail(t, "Scenario Associations",
"Expected to have %v Results. Has %v.", 2, len(results))
}
}
func TestICAssociations(t *testing.T) {

View file

@ -33,8 +33,8 @@ import (
// except the json tags that are needed for serializing the models
type Model struct {
ID uint `json:"id,omitempty" gorm:"primary_key:true"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"-" sql:"index"`
}
@ -72,6 +72,8 @@ type Scenario struct {
Dashboards []Dashboard `json:"-" gorm:"foreignkey:ScenarioID" `
// Files that belong to the Scenario (for example images, models, etc.)
Files []File `json:"-" gorm:"foreignkey:ScenarioID"`
// Results that belong to the Scenario
Results []Result `json:"-" gorm:"foreignkey:ScenarioID"`
}
// ComponentConfiguration data model
@ -215,3 +217,16 @@ type File struct {
// Width of an image file in pixels (optional)
ImageWidth int `json:"imageWidth" gorm:"default:0"`
}
// Result data model
type Result struct {
Model
// JSON snapshots of component configurations used to generate results
ConfigSnapshots postgres.Jsonb `json:"configSnapshots"`
// Description of results
Description string `json:"description"`
// ID of Scenario to which result belongs
ScenarioID uint `json:"scenarioID"`
// File IDs associated with result
ResultFileIDs pq.Int64Array `json:"resultFileIDs" gorm:"type:integer[]"`
}

View file

@ -45,6 +45,7 @@ const ModelWidget = ModelName("widget")
const ModelComponentConfiguration = ModelName("component-configuration")
const ModelSignal = ModelName("signal")
const ModelFile = ModelName("file")
const ModelResult = ModelName("result")
type CRUD string
@ -83,6 +84,7 @@ var Roles = RoleActions{
ModelDashboard: crud,
ModelSignal: crud,
ModelFile: crud,
ModelResult: crud,
},
"User": {
ModelUser: _ru_,
@ -95,6 +97,7 @@ var Roles = RoleActions{
ModelDashboard: crud,
ModelSignal: crud,
ModelFile: crud,
ModelResult: crud,
},
"Guest": {
ModelScenario: _r__,
@ -107,6 +110,7 @@ var Roles = RoleActions{
ModelUsers: none,
ModelSignal: _r__,
ModelFile: _r__,
ModelResult: none,
},
}

View file

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2020-11-11 16:32:47.799676915 +0100 CET m=+0.126448240
// 2021-01-12 17:04:27.751319166 +0100 CET m=+0.123714746
package docs
@ -806,6 +806,15 @@ var doc = `{
],
"produces": [
"text/plain",
"text/csv",
"application/gzip",
"application/x-gtar",
"application/x-tar",
"application/x-ustar",
"application/zip",
"application/msexcel",
"application/xml",
"application/x-bag",
"image/png",
"image/jpeg",
"image/gif",
@ -1439,6 +1448,445 @@ var doc = `{
}
}
},
"/results": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Get all results of scenario",
"operationId": "getResults",
"parameters": [
{
"type": "integer",
"description": "Scenario ID",
"name": "scenarioID",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Results which belong to scenario",
"schema": {
"$ref": "#/definitions/docs.ResponseResults"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Add a result to a scenario",
"operationId": "addResult",
"parameters": [
{
"description": "Result to be added incl. ID of Scenario",
"name": "inputResult",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/result.addResultRequest"
}
}
],
"responses": {
"200": {
"description": "Result that was added",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/results/{resultID}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Get a Result",
"operationId": "getResult",
"parameters": [
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was requested",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
},
"put": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Update a result",
"operationId": "updateResult",
"parameters": [
{
"description": "Result to be updated",
"name": "inputResult",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/result.updateResultRequest"
}
},
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was updated",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Delete a Result incl. all result files",
"operationId": "deleteResult",
"parameters": [
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was deleted",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/results/{resultID}/file": {
"post": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"text/plain",
"text/csv",
"application/gzip",
"application/x-gtar",
"application/x-tar",
"application/x-ustar",
"application/zip",
"application/msexcel",
"application/xml",
"application/x-bag"
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Upload a result file to the DB and associate it with scenario and result",
"operationId": "addResultFile",
"parameters": [
{
"type": "file",
"description": "File to be uploaded",
"name": "inputFile",
"in": "formData",
"required": true
},
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was updated",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/results/{resultID}/file/{fileID}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Delete a result file",
"operationId": "deleteResultFile",
"parameters": [
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "ID of the file to delete",
"name": "fileID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result for which file was deleted",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/scenarios": {
"get": {
"security": [
@ -2889,6 +3337,9 @@ var doc = `{
"database.ComponentConfiguration": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"fileIDs": {
"description": "Array of file IDs used by the component configuration",
"type": "string"
@ -2919,12 +3370,18 @@ var doc = `{
"startParameters": {
"description": "Start parameters of Component Configuration as JSON",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"database.Dashboard": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"grid": {
"description": "Grid of dashboard",
"type": "integer"
@ -2943,12 +3400,18 @@ var doc = `{
"scenarioID": {
"description": "ID of scenario to which dashboard belongs",
"type": "integer"
},
"updatedAt": {
"type": "string"
}
}
},
"database.File": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"date": {
"description": "Last modification time of file",
"type": "string"
@ -2956,6 +3419,14 @@ var doc = `{
"id": {
"type": "integer"
},
"imageHeight": {
"description": "Height of an image file in pixels (optional)",
"type": "integer"
},
"imageWidth": {
"description": "Width of an image file in pixels (optional)",
"type": "integer"
},
"name": {
"description": "Name of file",
"type": "string"
@ -2971,6 +3442,9 @@ var doc = `{
"type": {
"description": "Type of file (MIME type)",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
@ -2985,6 +3459,9 @@ var doc = `{
"description": "Category of IC (simulator, gateway, database, etc.)",
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"description": "Description of the IC",
"type": "string"
@ -3020,6 +3497,9 @@ var doc = `{
"description": "Type of IC (RTDS, VILLASnode, RTDS, etc.)",
"type": "string"
},
"updatedAt": {
"type": "string"
},
"uptime": {
"description": "Uptime of the IC",
"type": "number"
@ -3034,9 +3514,42 @@ var doc = `{
}
}
},
"database.Result": {
"type": "object",
"properties": {
"configSnapshots": {
"description": "JSON snapshots of component configurations used to generate results",
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"description": "Description of results",
"type": "string"
},
"id": {
"type": "integer"
},
"resultFileIDs": {
"description": "File IDs associated with result",
"type": "string"
},
"scenarioID": {
"description": "ID of Scenario to which result belongs",
"type": "integer"
},
"updatedAt": {
"type": "string"
}
}
},
"database.Scenario": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
@ -3051,6 +3564,9 @@ var doc = `{
"startParameters": {
"description": "Start parameters of scenario as JSON",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
@ -3061,6 +3577,9 @@ var doc = `{
"description": "ID of Component Configuration",
"type": "integer"
},
"createdAt": {
"type": "string"
},
"direction": {
"description": "Direction of the signal (in or out)",
"type": "string"
@ -3083,6 +3602,9 @@ var doc = `{
"unit": {
"description": "Unit of Signal",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
@ -3093,6 +3615,9 @@ var doc = `{
"description": "Indicating status of user (false means user is inactive and should not be able to login)",
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
@ -3104,6 +3629,9 @@ var doc = `{
"description": "Role of user",
"type": "string"
},
"updatedAt": {
"type": "string"
},
"username": {
"description": "Username of user",
"type": "string"
@ -3113,6 +3641,9 @@ var doc = `{
"database.Widget": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"customProperties": {
"description": "Custom properties of widget as JSON string",
"type": "string"
@ -3152,6 +3683,9 @@ var doc = `{
"description": "Type of widget",
"type": "string"
},
"updatedAt": {
"type": "string"
},
"width": {
"description": "Width of widget",
"type": "integer"
@ -3279,6 +3813,26 @@ var doc = `{
}
}
},
"docs.ResponseResult": {
"type": "object",
"properties": {
"result": {
"type": "object",
"$ref": "#/definitions/database.Result"
}
}
},
"docs.ResponseResults": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/database.Result"
}
}
}
},
"docs.ResponseScenario": {
"type": "object",
"properties": {
@ -3462,6 +4016,65 @@ var doc = `{
}
}
},
"result.addResultRequest": {
"type": "object",
"properties": {
"result": {
"type": "object",
"$ref": "#/definitions/result.validNewResult"
}
}
},
"result.updateResultRequest": {
"type": "object",
"properties": {
"result": {
"type": "object",
"$ref": "#/definitions/result.validUpdatedResult"
}
}
},
"result.validNewResult": {
"type": "object",
"required": [
"ConfigSnapshots",
"ScenarioID"
],
"properties": {
"ConfigSnapshots": {
"type": "string"
},
"Description": {
"type": "string"
},
"ResultFileIDs": {
"type": "array",
"items": {
"type": "integer"
}
},
"ScenarioID": {
"type": "integer"
}
}
},
"result.validUpdatedResult": {
"type": "object",
"properties": {
"configSnapshots": {
"type": "string"
},
"description": {
"type": "string"
},
"resultFileIDs": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"scenario.addScenarioRequest": {
"type": "object",
"properties": {

View file

@ -101,3 +101,11 @@ type ResponseFiles struct {
type ResponseFile struct {
file database.File
}
type ResponseResults struct {
results []database.Result
}
type ResponseResult struct {
result database.Result
}

View file

@ -789,6 +789,15 @@
],
"produces": [
"text/plain",
"text/csv",
"application/gzip",
"application/x-gtar",
"application/x-tar",
"application/x-ustar",
"application/zip",
"application/msexcel",
"application/xml",
"application/x-bag",
"image/png",
"image/jpeg",
"image/gif",
@ -1422,6 +1431,445 @@
}
}
},
"/results": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Get all results of scenario",
"operationId": "getResults",
"parameters": [
{
"type": "integer",
"description": "Scenario ID",
"name": "scenarioID",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Results which belong to scenario",
"schema": {
"$ref": "#/definitions/docs.ResponseResults"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Add a result to a scenario",
"operationId": "addResult",
"parameters": [
{
"description": "Result to be added incl. ID of Scenario",
"name": "inputResult",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/result.addResultRequest"
}
}
],
"responses": {
"200": {
"description": "Result that was added",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/results/{resultID}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Get a Result",
"operationId": "getResult",
"parameters": [
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was requested",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
},
"put": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Update a result",
"operationId": "updateResult",
"parameters": [
{
"description": "Result to be updated",
"name": "inputResult",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/result.updateResultRequest"
}
},
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was updated",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Delete a Result incl. all result files",
"operationId": "deleteResult",
"parameters": [
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was deleted",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/results/{resultID}/file": {
"post": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"text/plain",
"text/csv",
"application/gzip",
"application/x-gtar",
"application/x-tar",
"application/x-ustar",
"application/zip",
"application/msexcel",
"application/xml",
"application/x-bag"
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Upload a result file to the DB and associate it with scenario and result",
"operationId": "addResultFile",
"parameters": [
{
"type": "file",
"description": "File to be uploaded",
"name": "inputFile",
"in": "formData",
"required": true
},
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result that was updated",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/results/{resultID}/file/{fileID}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"results"
],
"summary": "Delete a result file",
"operationId": "deleteResultFile",
"parameters": [
{
"type": "integer",
"description": "Result ID",
"name": "resultID",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "ID of the file to delete",
"name": "fileID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Result for which file was deleted",
"schema": {
"$ref": "#/definitions/docs.ResponseResult"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"404": {
"description": "Not found",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"422": {
"description": "Unprocessable entity",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/docs.ResponseError"
}
}
}
}
},
"/scenarios": {
"get": {
"security": [
@ -2872,6 +3320,9 @@
"database.ComponentConfiguration": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"fileIDs": {
"description": "Array of file IDs used by the component configuration",
"type": "string"
@ -2902,12 +3353,18 @@
"startParameters": {
"description": "Start parameters of Component Configuration as JSON",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"database.Dashboard": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"grid": {
"description": "Grid of dashboard",
"type": "integer"
@ -2926,12 +3383,18 @@
"scenarioID": {
"description": "ID of scenario to which dashboard belongs",
"type": "integer"
},
"updatedAt": {
"type": "string"
}
}
},
"database.File": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"date": {
"description": "Last modification time of file",
"type": "string"
@ -2939,6 +3402,14 @@
"id": {
"type": "integer"
},
"imageHeight": {
"description": "Height of an image file in pixels (optional)",
"type": "integer"
},
"imageWidth": {
"description": "Width of an image file in pixels (optional)",
"type": "integer"
},
"name": {
"description": "Name of file",
"type": "string"
@ -2954,6 +3425,9 @@
"type": {
"description": "Type of file (MIME type)",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
@ -2968,6 +3442,9 @@
"description": "Category of IC (simulator, gateway, database, etc.)",
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"description": "Description of the IC",
"type": "string"
@ -3003,6 +3480,9 @@
"description": "Type of IC (RTDS, VILLASnode, RTDS, etc.)",
"type": "string"
},
"updatedAt": {
"type": "string"
},
"uptime": {
"description": "Uptime of the IC",
"type": "number"
@ -3017,9 +3497,42 @@
}
}
},
"database.Result": {
"type": "object",
"properties": {
"configSnapshots": {
"description": "JSON snapshots of component configurations used to generate results",
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"description": "Description of results",
"type": "string"
},
"id": {
"type": "integer"
},
"resultFileIDs": {
"description": "File IDs associated with result",
"type": "string"
},
"scenarioID": {
"description": "ID of Scenario to which result belongs",
"type": "integer"
},
"updatedAt": {
"type": "string"
}
}
},
"database.Scenario": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
@ -3034,6 +3547,9 @@
"startParameters": {
"description": "Start parameters of scenario as JSON",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
@ -3044,6 +3560,9 @@
"description": "ID of Component Configuration",
"type": "integer"
},
"createdAt": {
"type": "string"
},
"direction": {
"description": "Direction of the signal (in or out)",
"type": "string"
@ -3066,6 +3585,9 @@
"unit": {
"description": "Unit of Signal",
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
@ -3076,6 +3598,9 @@
"description": "Indicating status of user (false means user is inactive and should not be able to login)",
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
@ -3087,6 +3612,9 @@
"description": "Role of user",
"type": "string"
},
"updatedAt": {
"type": "string"
},
"username": {
"description": "Username of user",
"type": "string"
@ -3096,6 +3624,9 @@
"database.Widget": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"customProperties": {
"description": "Custom properties of widget as JSON string",
"type": "string"
@ -3135,6 +3666,9 @@
"description": "Type of widget",
"type": "string"
},
"updatedAt": {
"type": "string"
},
"width": {
"description": "Width of widget",
"type": "integer"
@ -3262,6 +3796,26 @@
}
}
},
"docs.ResponseResult": {
"type": "object",
"properties": {
"result": {
"type": "object",
"$ref": "#/definitions/database.Result"
}
}
},
"docs.ResponseResults": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/database.Result"
}
}
}
},
"docs.ResponseScenario": {
"type": "object",
"properties": {
@ -3445,6 +3999,65 @@
}
}
},
"result.addResultRequest": {
"type": "object",
"properties": {
"result": {
"type": "object",
"$ref": "#/definitions/result.validNewResult"
}
}
},
"result.updateResultRequest": {
"type": "object",
"properties": {
"result": {
"type": "object",
"$ref": "#/definitions/result.validUpdatedResult"
}
}
},
"result.validNewResult": {
"type": "object",
"required": [
"ConfigSnapshots",
"ScenarioID"
],
"properties": {
"ConfigSnapshots": {
"type": "string"
},
"Description": {
"type": "string"
},
"ResultFileIDs": {
"type": "array",
"items": {
"type": "integer"
}
},
"ScenarioID": {
"type": "integer"
}
}
},
"result.validUpdatedResult": {
"type": "object",
"properties": {
"configSnapshots": {
"type": "string"
},
"description": {
"type": "string"
},
"resultFileIDs": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"scenario.addScenarioRequest": {
"type": "object",
"properties": {

View file

@ -83,6 +83,8 @@ definitions:
type: object
database.ComponentConfiguration:
properties:
createdAt:
type: string
fileIDs:
description: Array of file IDs used by the component configuration
type: string
@ -106,9 +108,13 @@ definitions:
startParameters:
description: Start parameters of Component Configuration as JSON
type: string
updatedAt:
type: string
type: object
database.Dashboard:
properties:
createdAt:
type: string
grid:
description: Grid of dashboard
type: integer
@ -123,14 +129,24 @@ definitions:
scenarioID:
description: ID of scenario to which dashboard belongs
type: integer
updatedAt:
type: string
type: object
database.File:
properties:
createdAt:
type: string
date:
description: Last modification time of file
type: string
id:
type: integer
imageHeight:
description: Height of an image file in pixels (optional)
type: integer
imageWidth:
description: Width of an image file in pixels (optional)
type: integer
name:
description: Name of file
type: string
@ -143,6 +159,8 @@ definitions:
type:
description: Type of file (MIME type)
type: string
updatedAt:
type: string
type: object
database.InfrastructureComponent:
properties:
@ -152,6 +170,8 @@ definitions:
category:
description: Category of IC (simulator, gateway, database, etc.)
type: string
createdAt:
type: string
description:
description: Description of the IC
type: string
@ -178,6 +198,8 @@ definitions:
type:
description: Type of IC (RTDS, VILLASnode, RTDS, etc.)
type: string
updatedAt:
type: string
uptime:
description: Uptime of the IC
type: number
@ -188,8 +210,31 @@ definitions:
description: WebsocketURL if the IC
type: string
type: object
database.Result:
properties:
configSnapshots:
description: JSON snapshots of component configurations used to generate results
type: string
createdAt:
type: string
description:
description: Description of results
type: string
id:
type: integer
resultFileIDs:
description: File IDs associated with result
type: string
scenarioID:
description: ID of Scenario to which result belongs
type: integer
updatedAt:
type: string
type: object
database.Scenario:
properties:
createdAt:
type: string
id:
type: integer
name:
@ -201,12 +246,16 @@ definitions:
startParameters:
description: Start parameters of scenario as JSON
type: string
updatedAt:
type: string
type: object
database.Signal:
properties:
configID:
description: ID of Component Configuration
type: integer
createdAt:
type: string
direction:
description: Direction of the signal (in or out)
type: string
@ -224,6 +273,8 @@ definitions:
unit:
description: Unit of Signal
type: string
updatedAt:
type: string
type: object
database.User:
properties:
@ -231,6 +282,8 @@ definitions:
description: Indicating status of user (false means user is inactive and should
not be able to login)
type: boolean
createdAt:
type: string
id:
type: integer
mail:
@ -239,12 +292,16 @@ definitions:
role:
description: Role of user
type: string
updatedAt:
type: string
username:
description: Username of user
type: string
type: object
database.Widget:
properties:
createdAt:
type: string
customProperties:
description: Custom properties of widget as JSON string
type: string
@ -274,6 +331,8 @@ definitions:
type:
description: Type of widget
type: string
updatedAt:
type: string
width:
description: Width of widget
type: integer
@ -358,6 +417,19 @@ definitions:
$ref: '#/definitions/database.InfrastructureComponent'
type: array
type: object
docs.ResponseResult:
properties:
result:
$ref: '#/definitions/database.Result'
type: object
type: object
docs.ResponseResults:
properties:
results:
items:
$ref: '#/definitions/database.Result'
type: array
type: object
docs.ResponseScenario:
properties:
scenario:
@ -479,6 +551,45 @@ definitions:
WebsocketURL:
type: string
type: object
result.addResultRequest:
properties:
result:
$ref: '#/definitions/result.validNewResult'
type: object
type: object
result.updateResultRequest:
properties:
result:
$ref: '#/definitions/result.validUpdatedResult'
type: object
type: object
result.validNewResult:
properties:
ConfigSnapshots:
type: string
Description:
type: string
ResultFileIDs:
items:
type: integer
type: array
ScenarioID:
type: integer
required:
- ConfigSnapshots
- ScenarioID
type: object
result.validUpdatedResult:
properties:
configSnapshots:
type: string
description:
type: string
resultFileIDs:
items:
type: integer
type: array
type: object
scenario.addScenarioRequest:
properties:
scenario:
@ -1244,6 +1355,15 @@ paths:
type: integer
produces:
- text/plain
- text/csv
- application/gzip
- application/x-gtar
- application/x-tar
- application/x-ustar
- application/zip
- application/msexcel
- application/xml
- application/x-bag
- image/png
- image/jpeg
- image/gif
@ -1616,6 +1736,292 @@ paths:
summary: Prometheus metrics endpoint
tags:
- metrics
/results:
get:
operationId: getResults
parameters:
- description: Scenario ID
in: query
name: scenarioID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Results which belong to scenario
schema:
$ref: '#/definitions/docs.ResponseResults'
"404":
description: Not found
schema:
$ref: '#/definitions/docs.ResponseError'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/docs.ResponseError'
"500":
description: Internal server error
schema:
$ref: '#/definitions/docs.ResponseError'
security:
- Bearer: []
summary: Get all results of scenario
tags:
- results
post:
consumes:
- application/json
operationId: addResult
parameters:
- description: Result to be added incl. ID of Scenario
in: body
name: inputResult
required: true
schema:
$ref: '#/definitions/result.addResultRequest'
type: object
produces:
- application/json
responses:
"200":
description: Result that was added
schema:
$ref: '#/definitions/docs.ResponseResult'
"400":
description: Bad request
schema:
$ref: '#/definitions/docs.ResponseError'
"404":
description: Not found
schema:
$ref: '#/definitions/docs.ResponseError'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/docs.ResponseError'
"500":
description: Internal server error
schema:
$ref: '#/definitions/docs.ResponseError'
security:
- Bearer: []
summary: Add a result to a scenario
tags:
- results
/results/{resultID}:
delete:
operationId: deleteResult
parameters:
- description: Result ID
in: path
name: resultID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Result that was deleted
schema:
$ref: '#/definitions/docs.ResponseResult'
"400":
description: Bad request
schema:
$ref: '#/definitions/docs.ResponseError'
"404":
description: Not found
schema:
$ref: '#/definitions/docs.ResponseError'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/docs.ResponseError'
"500":
description: Internal server error
schema:
$ref: '#/definitions/docs.ResponseError'
security:
- Bearer: []
summary: Delete a Result incl. all result files
tags:
- results
get:
operationId: getResult
parameters:
- description: Result ID
in: path
name: resultID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Result that was requested
schema:
$ref: '#/definitions/docs.ResponseResult'
"400":
description: Bad request
schema:
$ref: '#/definitions/docs.ResponseError'
"404":
description: Not found
schema:
$ref: '#/definitions/docs.ResponseError'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/docs.ResponseError'
"500":
description: Internal server error
schema:
$ref: '#/definitions/docs.ResponseError'
security:
- Bearer: []
summary: Get a Result
tags:
- results
put:
consumes:
- application/json
operationId: updateResult
parameters:
- description: Result to be updated
in: body
name: inputResult
required: true
schema:
$ref: '#/definitions/result.updateResultRequest'
type: object
- description: Result ID
in: path
name: resultID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Result that was updated
schema:
$ref: '#/definitions/docs.ResponseResult'
"400":
description: Bad request
schema:
$ref: '#/definitions/docs.ResponseError'
"404":
description: Not found
schema:
$ref: '#/definitions/docs.ResponseError'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/docs.ResponseError'
"500":
description: Internal server error
schema:
$ref: '#/definitions/docs.ResponseError'
security:
- Bearer: []
summary: Update a result
tags:
- results
/results/{resultID}/file:
post:
consumes:
- text/plain
- text/csv
- application/gzip
- application/x-gtar
- application/x-tar
- application/x-ustar
- application/zip
- application/msexcel
- application/xml
- application/x-bag
operationId: addResultFile
parameters:
- description: File to be uploaded
in: formData
name: inputFile
required: true
type: file
- description: Result ID
in: path
name: resultID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Result that was updated
schema:
$ref: '#/definitions/docs.ResponseResult'
"400":
description: Bad request
schema:
$ref: '#/definitions/docs.ResponseError'
"404":
description: Not found
schema:
$ref: '#/definitions/docs.ResponseError'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/docs.ResponseError'
"500":
description: Internal server error
schema:
$ref: '#/definitions/docs.ResponseError'
security:
- Bearer: []
summary: Upload a result file to the DB and associate it with scenario and result
tags:
- results
/results/{resultID}/file/{fileID}:
delete:
operationId: deleteResultFile
parameters:
- description: Result ID
in: path
name: resultID
required: true
type: integer
- description: ID of the file to delete
in: path
name: fileID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Result for which file was deleted
schema:
$ref: '#/definitions/docs.ResponseResult'
"400":
description: Bad request
schema:
$ref: '#/definitions/docs.ResponseError'
"404":
description: Not found
schema:
$ref: '#/definitions/docs.ResponseError'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/docs.ResponseError'
"500":
description: Internal server error
schema:
$ref: '#/definitions/docs.ResponseError'
security:
- Bearer: []
summary: Delete a result file
tags:
- results
/scenarios:
get:
operationId: getScenarios

1
go.sum
View file

@ -258,6 +258,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View file

@ -251,7 +251,7 @@ var FileD = database.File{
var customPropertiesBox = json.RawMessage(`{"border_color" : "#4287f5", "border_color_opacity": 1, "background_color" : "#961520", "background_color_opacity" : 1}`)
var customPropertiesSlider = json.RawMessage(`{"default_value" : 0, "orientation" : 0, "rangeUseMinMax": false, "rangeMin" : 0, "rangeMax": 200, "rangeUseMinMax" : true, "showUnit": true, "continous_update": false, "value": "", "resizeLeftRightLock": false, "resizeTopBottomLock": true, "step": 0.1 }`)
var customPropertiesLabel = json.RawMessage(`{"textSize" : "20", "fontColor" : "#4287f5", "fontColor_opacity": 1}`)
var customPropertiesButton = json.RawMessage(`{"pressed": false, "toggle" : false, "on_value" : 1, "off_value" : 0, "background_color": "#961520", "font_color": "#4287f5"}`)
var customPropertiesButton = json.RawMessage(`{"pressed": false, "toggle" : false, "on_value" : 1, "off_value" : 0, "background_color": "#961520", "font_color": "#4287f5", "border_color": "#4287f5", "background_color_opacity": 1}`)
var customPropertiesLamp = json.RawMessage(`{"signal" : 0, "on_color" : "#4287f5", "off_color": "#961520", "threshold" : 0.5, "on_color_opacity": 1, "off_color_opacity": 1}`)
var WidgetA = database.Widget{

View file

@ -131,7 +131,6 @@ func (m *ComponentConfiguration) delete() error {
}
// remove association between ComponentConfiguration and Scenario
// ComponentConfiguration itself is not deleted from DB, it remains as "dangling"
err = db.Model(&so).Association("ComponentConfigurations").Delete(m).Error
if err != nil {
return err
@ -143,6 +142,32 @@ func (m *ComponentConfiguration) delete() error {
return err
}
// Get Signals of InputMapping and delete them
var InputMappingSignals []database.Signal
err = db.Model(m).Related(&InputMappingSignals, "InputMapping").Error
if err != nil {
return err
}
for sig, _ := range InputMappingSignals {
err = db.Delete(&sig).Error
if err != nil {
return err
}
}
// Get Signals of OutputMapping and delete them
var OutputMappingSignals []database.Signal
err = db.Model(m).Related(&OutputMappingSignals, "OutputMapping").Error
if err != nil {
return err
}
for sig, _ := range OutputMappingSignals {
err = db.Delete(&sig).Error
if err != nil {
return err
}
}
// delete component configuration
err = db.Delete(m).Error
if err != nil {

View file

@ -30,9 +30,9 @@ type Dashboard struct {
database.Dashboard
}
func (v *Dashboard) save() error {
func (d *Dashboard) save() error {
db := database.GetDB()
err := db.Create(v).Error
err := db.Create(d).Error
return err
}
@ -88,8 +88,22 @@ func (d *Dashboard) delete() error {
}
// remove association between Dashboard and Scenario
// Dashboard itself is not deleted from DB, it remains as "dangling"
err = db.Model(&sim).Association("Dashboards").Delete(d).Error
// get all widgets of the dashboard
var widgets []database.Widget
err = db.Order("ID asc").Model(d).Related(&widgets, "Widgets").Error
if err != nil {
return err
}
// Delete widgets
for widget, _ := range widgets {
err = db.Delete(&widget).Error
}
// Delete dashboard
err = db.Delete(d).Error
return err
}

View file

@ -104,7 +104,7 @@ func addFile(c *gin.Context) {
}
var newFile File
err = newFile.register(file_header, so.ID)
err = newFile.Register(file_header, so.ID)
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"file": newFile.File})
}
@ -116,6 +116,15 @@ func addFile(c *gin.Context) {
// @ID getFile
// @Tags files
// @Produce text/plain
// @Produce text/csv
// @Produce application/gzip
// @Produce application/x-gtar
// @Produce application/x-tar
// @Produce application/x-ustar
// @Produce application/zip
// @Produce application/msexcel
// @Produce application/xml
// @Produce application/x-bag
// @Produce png
// @Produce jpeg
// @Produce gif
@ -132,7 +141,7 @@ func addFile(c *gin.Context) {
func getFile(c *gin.Context) {
// check access
ok, f := checkPermissions(c, database.Read)
ok, f := CheckPermissions(c, database.Read)
if !ok {
return
}
@ -165,7 +174,7 @@ func getFile(c *gin.Context) {
func updateFile(c *gin.Context) {
// check access
ok, f := checkPermissions(c, database.Update)
ok, f := CheckPermissions(c, database.Update)
if !ok {
return
}
@ -199,12 +208,12 @@ func updateFile(c *gin.Context) {
func deleteFile(c *gin.Context) {
// check access
ok, f := checkPermissions(c, database.Delete)
ok, f := CheckPermissions(c, database.Delete)
if !ok {
return
}
err := f.delete()
err := f.Delete()
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"file": f.File})
}

View file

@ -46,7 +46,7 @@ type File struct {
database.File
}
func (f *File) byID(id uint) error {
func (f *File) ByID(id uint) error {
db := database.GetDB()
err := db.Find(f, id).Error
if err != nil {
@ -82,7 +82,8 @@ func (f *File) download(c *gin.Context) error {
return nil
}
func (f *File) register(fileHeader *multipart.FileHeader, scenarioID uint) error {
func (f *File) Register(fileHeader *multipart.FileHeader, scenarioID uint) error {
// Obtain properties of file
f.Type = fileHeader.Header.Get("Content-Type")
f.Name = filepath.Base(fileHeader.Filename)
@ -200,7 +201,7 @@ func (f *File) update(fileHeader *multipart.FileHeader) error {
return nil
}
func (f *File) delete() error {
func (f *File) Delete() error {
db := database.GetDB()

View file

@ -29,7 +29,7 @@ import (
"github.com/gin-gonic/gin"
)
func checkPermissions(c *gin.Context, operation database.CRUD) (bool, File) {
func CheckPermissions(c *gin.Context, operation database.CRUD) (bool, File) {
var f File
@ -44,7 +44,7 @@ func checkPermissions(c *gin.Context, operation database.CRUD) (bool, File) {
return false, f
}
err = f.byID(uint(fileID))
err = f.ByID(uint(fileID))
if helper.DBError(c, err) {
return false, f
}

View file

@ -39,6 +39,7 @@ import (
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/healthz"
infrastructure_component "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/infrastructure-component"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/metrics"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/result"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/signal"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user"
@ -68,6 +69,7 @@ func RegisterEndpoints(router *gin.Engine, api *gin.RouterGroup) {
file.RegisterFileEndpoints(api.Group("/files"))
user.RegisterUserEndpoints(api.Group("/users"))
infrastructure_component.RegisterICEndpoints(api.Group("/ic"))
result.RegisterResultEndpoints(api.Group("/results"))
router.GET("swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

View file

@ -0,0 +1,320 @@
/** Result package, endpoints.
*
* @author Sonja Happ <sonja.happ@eonerc.rwth-aachen.de>
* @copyright 2014-2019, Institute for Automation of Complex Power Systems, EONERC
* @license GNU General Public License (version 3)
*
* VILLASweb-backend-go
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
package result
import (
"fmt"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/database"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/helper"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/file"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario"
"github.com/gin-gonic/gin"
"net/http"
)
func RegisterResultEndpoints(r *gin.RouterGroup) {
r.GET("", getResults)
r.POST("", addResult)
r.PUT("/:resultID", updateResult)
r.GET("/:resultID", getResult)
r.DELETE("/:resultID", deleteResult)
r.POST("/:resultID/file", addResultFile)
r.DELETE("/:resultID/file/:fileID", deleteResultFile)
}
// getResults godoc
// @Summary Get all results of scenario
// @ID getResults
// @Produce json
// @Tags results
// @Success 200 {object} docs.ResponseResults "Results which belong to scenario"
// @Failure 404 {object} docs.ResponseError "Not found"
// @Failure 422 {object} docs.ResponseError "Unprocessable entity"
// @Failure 500 {object} docs.ResponseError "Internal server error"
// @Param scenarioID query int true "Scenario ID"
// @Router /results [get]
// @Security Bearer
func getResults(c *gin.Context) {
ok, sco := scenario.CheckPermissions(c, database.Read, "query", -1)
if !ok {
return
}
db := database.GetDB()
var results []database.Result
err := db.Order("ID asc").Model(sco).Related(&results, "Results").Error
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"results": results})
}
}
// addResult godoc
// @Summary Add a result to a scenario
// @ID addResult
// @Accept json
// @Produce json
// @Tags results
// @Success 200 {object} docs.ResponseResult "Result that was added"
// @Failure 400 {object} docs.ResponseError "Bad request"
// @Failure 404 {object} docs.ResponseError "Not found"
// @Failure 422 {object} docs.ResponseError "Unprocessable entity"
// @Failure 500 {object} docs.ResponseError "Internal server error"
// @Param inputResult body result.addResultRequest true "Result to be added incl. ID of Scenario"
// @Router /results [post]
// @Security Bearer
func addResult(c *gin.Context) {
// bind request to JSON
var req addResultRequest
if err := c.ShouldBindJSON(&req); err != nil {
helper.BadRequestError(c, err.Error())
return
}
// Validate the request
if err := req.validate(); err != nil {
helper.UnprocessableEntityError(c, err.Error())
return
}
// Create the new result from the request
newResult := req.createResult()
// Check if user is allowed to modify scenario specified in request
ok, _ := scenario.CheckPermissions(c, database.Update, "body", int(newResult.ScenarioID))
if !ok {
return
}
// add result to DB and add association to scenario
err := newResult.addToScenario()
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"result": newResult.Result})
}
}
// updateResult godoc
// @Summary Update a result
// @ID updateResult
// @Tags results
// @Accept json
// @Produce json
// @Success 200 {object} docs.ResponseResult "Result that was updated"
// @Failure 400 {object} docs.ResponseError "Bad request"
// @Failure 404 {object} docs.ResponseError "Not found"
// @Failure 422 {object} docs.ResponseError "Unprocessable entity"
// @Failure 500 {object} docs.ResponseError "Internal server error"
// @Param inputResult body result.updateResultRequest true "Result to be updated"
// @Param resultID path int true "Result ID"
// @Router /results/{resultID} [put]
// @Security Bearer
func updateResult(c *gin.Context) {
ok, oldResult := checkPermissions(c, database.Update, "path", -1)
if !ok {
return
}
var req updateResultRequest
if err := c.ShouldBindJSON(&req); err != nil {
helper.BadRequestError(c, err.Error())
return
}
// Validate the request
if err := req.Result.validate(); err != nil {
helper.BadRequestError(c, err.Error())
return
}
// Create the updatedResult from oldResult
updatedResult := req.updatedResult(oldResult)
// update the Result in the DB
err := oldResult.update(updatedResult)
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"result": updatedResult.Result})
}
}
// getResult godoc
// @Summary Get a Result
// @ID getResult
// @Tags results
// @Produce json
// @Success 200 {object} docs.ResponseResult "Result that was requested"
// @Failure 400 {object} docs.ResponseError "Bad request"
// @Failure 404 {object} docs.ResponseError "Not found"
// @Failure 422 {object} docs.ResponseError "Unprocessable entity"
// @Failure 500 {object} docs.ResponseError "Internal server error"
// @Param resultID path int true "Result ID"
// @Router /results/{resultID} [get]
// @Security Bearer
func getResult(c *gin.Context) {
ok, result := checkPermissions(c, database.Read, "path", -1)
if !ok {
return
}
c.JSON(http.StatusOK, gin.H{"result": result.Result})
}
// deleteResult godoc
// @Summary Delete a Result incl. all result files
// @ID deleteResult
// @Tags results
// @Produce json
// @Success 200 {object} docs.ResponseResult "Result that was deleted"
// @Failure 400 {object} docs.ResponseError "Bad request"
// @Failure 404 {object} docs.ResponseError "Not found"
// @Failure 422 {object} docs.ResponseError "Unprocessable entity"
// @Failure 500 {object} docs.ResponseError "Internal server error"
// @Param resultID path int true "Result ID"
// @Router /results/{resultID} [delete]
// @Security Bearer
func deleteResult(c *gin.Context) {
ok, result := checkPermissions(c, database.Delete, "path", -1)
if !ok {
return
}
// Check if user is allowed to modify scenario associated with result
ok, _ = scenario.CheckPermissions(c, database.Update, "body", int(result.ScenarioID))
if !ok {
return
}
err := result.delete()
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"result": result.Result})
}
}
// addResultFile godoc
// @Summary Upload a result file to the DB and associate it with scenario and result
// @ID addResultFile
// @Tags results
// @Accept text/plain
// @Accept text/csv
// @Accept application/gzip
// @Accept application/x-gtar
// @Accept application/x-tar
// @Accept application/x-ustar
// @Accept application/zip
// @Accept application/msexcel
// @Accept application/xml
// @Accept application/x-bag
// @Produce json
// @Success 200 {object} docs.ResponseResult "Result that was updated"
// @Failure 400 {object} docs.ResponseError "Bad request"
// @Failure 404 {object} docs.ResponseError "Not found"
// @Failure 422 {object} docs.ResponseError "Unprocessable entity"
// @Failure 500 {object} docs.ResponseError "Internal server error"
// @Param inputFile formData file true "File to be uploaded"
// @Param resultID path int true "Result ID"
// @Router /results/{resultID}/file [post]
// @Security Bearer
func addResultFile(c *gin.Context) {
ok, result := checkPermissions(c, database.Update, "path", -1)
if !ok {
return
}
// Check if user is allowed to modify scenario associated with result
ok, sco := scenario.CheckPermissions(c, database.Update, "body", int(result.ScenarioID))
if !ok {
return
}
// Extract file from POST request form
file_header, err := c.FormFile("file")
if err != nil {
helper.BadRequestError(c, fmt.Sprintf("Get form error: %s", err.Error()))
return
}
// save result file to DB and associate it with scenario
var newFile file.File
err = newFile.Register(file_header, sco.ID)
if helper.DBError(c, err) {
return
}
// add file ID to ResultFileIDs of Result
err = result.addResultFileID(newFile.File.ID)
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"result": result.Result})
}
}
// deleteResultFile godoc
// @Summary Delete a result file
// @ID deleteResultFile
// @Tags results
// @Produce json
// @Success 200 {object} docs.ResponseResult "Result for which file was deleted"
// @Failure 400 {object} docs.ResponseError "Bad request"
// @Failure 404 {object} docs.ResponseError "Not found"
// @Failure 422 {object} docs.ResponseError "Unprocessable entity"
// @Failure 500 {object} docs.ResponseError "Internal server error"
// @Param resultID path int true "Result ID"
// @Param fileID path int true "ID of the file to delete"
// @Router /results/{resultID}/file/{fileID} [delete]
// @Security Bearer
func deleteResultFile(c *gin.Context) {
// check access
ok, result := checkPermissions(c, database.Update, "path", -1)
if !ok {
return
}
ok, f := file.CheckPermissions(c, database.Delete)
if !ok {
return
}
// Check if user is allowed to modify scenario associated with result
ok, _ = scenario.CheckPermissions(c, database.Update, "body", int(result.ScenarioID))
if !ok {
return
}
// remove file ID from ResultFileIDs of Result
err := result.removeResultFileID(f.ID)
if helper.DBError(c, err) {
return
}
// Delete the file
err = f.Delete()
if !helper.DBError(c, err) {
c.JSON(http.StatusOK, gin.H{"result": result.Result})
}
}

View file

@ -0,0 +1,148 @@
/** Result package, methods.
*
* @author Sonja Happ <sonja.happ@eonerc.rwth-aachen.de>
* @copyright 2014-2019, Institute for Automation of Complex Power Systems, EONERC
* @license GNU General Public License (version 3)
*
* VILLASweb-backend-go
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
package result
import (
"git.rwth-aachen.de/acs/public/villas/web-backend-go/database"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/file"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario"
"log"
)
type Result struct {
database.Result
}
func (r *Result) save() error {
db := database.GetDB()
err := db.Create(r).Error
return err
}
func (r *Result) ByID(id uint) error {
db := database.GetDB()
err := db.Find(r, id).Error
if err != nil {
return err
}
return nil
}
func (r *Result) addToScenario() error {
db := database.GetDB()
var sco scenario.Scenario
err := sco.ByID(r.ScenarioID)
if err != nil {
return err
}
// save result to DB
err = r.save()
if err != nil {
return err
}
// associate result with scenario
err = db.Model(&sco).Association("Results").Append(r).Error
return err
}
func (r *Result) update(modifiedResult Result) error {
db := database.GetDB()
err := db.Model(r).Updates(map[string]interface{}{
"Description": modifiedResult.Description,
"ConfigSnapshots": modifiedResult.ConfigSnapshots,
"ResultFileIDs": modifiedResult.ResultFileIDs,
}).Error
return err
}
func (r *Result) delete() error {
db := database.GetDB()
var sco scenario.Scenario
err := sco.ByID(r.ScenarioID)
if err != nil {
return err
}
// remove association between Result and Scenario
err = db.Model(&sco).Association("Results").Delete(r).Error
// Delete result files
for _, fileid := range r.ResultFileIDs {
var f file.File
err := f.ByID(uint(fileid))
if err != nil {
log.Println("Unable to delete file with ID ", fileid, err)
continue
}
err = f.Delete()
if err != nil {
return err
}
}
// Delete result
err = db.Delete(r).Error
return err
}
func (r *Result) addResultFileID(fileID uint) error {
oldResultFileIDs := r.ResultFileIDs
newResultFileIDs := append(oldResultFileIDs, int64(fileID))
db := database.GetDB()
err := db.Model(r).Updates(map[string]interface{}{
"ResultFileIDs": newResultFileIDs,
}).Error
return err
}
func (r *Result) removeResultFileID(fileID uint) error {
oldResultFileIDs := r.ResultFileIDs
var newResultFileIDs []int64
for _, id := range oldResultFileIDs {
if id != int64(fileID) {
newResultFileIDs = append(newResultFileIDs, id)
}
}
db := database.GetDB()
err := db.Model(r).Updates(map[string]interface{}{
"ResultFileIDs": newResultFileIDs,
}).Error
return err
}

View file

@ -0,0 +1,59 @@
/** Result package, middleware.
*
* @author Sonja Happ <sonja.happ@eonerc.rwth-aachen.de>
* @copyright 2014-2019, Institute for Automation of Complex Power Systems, EONERC
* @license GNU General Public License (version 3)
*
* VILLASweb-backend-go
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
package result
import (
"fmt"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/database"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/helper"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario"
"github.com/gin-gonic/gin"
)
func checkPermissions(c *gin.Context, operation database.CRUD, resultIDSource string, resultIDBody int) (bool, Result) {
var result Result
err := database.ValidateRole(c, database.ModelResult, operation)
if err != nil {
helper.UnprocessableEntityError(c, fmt.Sprintf("Access denied (role validation failed): %v", err.Error()))
return false, result
}
resultID, err := helper.GetIDOfElement(c, "resultID", resultIDSource, resultIDBody)
if err != nil {
return false, result
}
err = result.ByID(uint(resultID))
if helper.DBError(c, err) {
return false, result
}
ok, _ := scenario.CheckPermissions(c, operation, "body", int(result.ScenarioID))
if !ok {
return false, result
}
return true, result
}

View file

@ -0,0 +1,506 @@
/** Result package, testing.
*
* @author Sonja Happ <sonja.happ@eonerc.rwth-aachen.de>
* @copyright 2014-2019, Institute for Automation of Complex Power Systems, EONERC
* @license GNU General Public License (version 3)
*
* VILLASweb-backend-go
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
package result
import (
"bytes"
"encoding/json"
"fmt"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/database"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/helper"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/file"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario"
"git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm/dialects/postgres"
"github.com/stretchr/testify/assert"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"testing"
)
var router *gin.Engine
var base_api_results = "/api/results"
var base_api_auth = "/api/authenticate"
type ScenarioRequest struct {
Name string `json:"name,omitempty"`
Running bool `json:"running,omitempty"`
StartParameters postgres.Jsonb `json:"startParameters,omitempty"`
}
type ResultRequest struct {
Description string `json:"description,omitempty"`
ScenarioID uint `json:"scenarioID,omitempty"`
ConfigSnapshots postgres.Jsonb `json:"configSnapshots,omitempty"`
}
type ResponseResult struct {
Result database.Result `json:"result"`
}
func addScenario() (scenarioID uint) {
// authenticate as admin
token, _ := helper.AuthenticateForTest(router,
"/api/authenticate", "POST", helper.AdminCredentials)
// authenticate as normal user
token, _ = helper.AuthenticateForTest(router,
"/api/authenticate", "POST", helper.UserACredentials)
// POST $newScenario
newScenario := ScenarioRequest{
Name: helper.ScenarioA.Name,
Running: helper.ScenarioA.Running,
StartParameters: helper.ScenarioA.StartParameters,
}
_, resp, _ := helper.TestEndpoint(router, token,
"/api/scenarios", "POST", helper.KeyModels{"scenario": newScenario})
// Read newScenario's ID from the response
newScenarioID, _ := helper.GetResponseID(resp)
// add the guest user to the new scenario
_, resp, _ = helper.TestEndpoint(router, token,
fmt.Sprintf("/api/scenarios/%v/user?username=User_C", newScenarioID), "PUT", nil)
return uint(newScenarioID)
}
func TestMain(m *testing.M) {
err := configuration.InitConfig()
if err != nil {
panic(m)
}
err = database.InitDB(configuration.GolbalConfig)
if err != nil {
panic(m)
}
defer database.DBpool.Close()
router = gin.Default()
api := router.Group("/api")
user.RegisterAuthenticate(api.Group("/authenticate"))
api.Use(user.Authentication(true))
// scenario endpoints required here to first add a scenario to the DB
scenario.RegisterScenarioEndpoints(api.Group("/scenarios"))
// file endpoints required to download result file
file.RegisterFileEndpoints(api.Group("/files"))
RegisterResultEndpoints(api.Group("/results"))
os.Exit(m.Run())
}
func TestGetAllResultsOfScenario(t *testing.T) {
database.DropTables()
database.MigrateModels()
assert.NoError(t, helper.DBAddAdminAndUserAndGuest())
// prepare the content of the DB for testing
// by adding a scenario
scenarioID := addScenario()
// authenticate as normal user
token, err := helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserACredentials)
assert.NoError(t, err)
// test POST newResult
configSnapshot1 := json.RawMessage(`{"configs": [ {"Name" : "conf1", "scenarioID" : 1}, {"Name" : "conf2", "scenarioID" : 1}]}`)
confSnapshots := postgres.Jsonb{configSnapshot1}
newResult := ResultRequest{
Description: "This is a test result.",
ScenarioID: scenarioID,
ConfigSnapshots: confSnapshots,
}
code, resp, err := helper.TestEndpoint(router, token,
base_api_results, "POST", helper.KeyModels{"result": newResult})
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Count the number of all the results returned for scenario
NumberOfConfigs, err := helper.LengthOfResponse(router, token,
fmt.Sprintf("%v?scenarioID=%v", base_api_results, scenarioID), "GET", nil)
assert.NoError(t, err)
assert.Equal(t, 1, NumberOfConfigs)
// authenticate as normal userB who has no access to scenario
token, err = helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserBCredentials)
assert.NoError(t, err)
// try to get results without access
// should result in unprocessable entity
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v?scenarioID=%v", base_api_results, scenarioID), "GET", nil)
assert.NoError(t, err)
assert.Equalf(t, 422, code, "Response body: \n%v\n", resp)
}
func TestAddGetUpdateDeleteResult(t *testing.T) {
database.DropTables()
database.MigrateModels()
assert.NoError(t, helper.DBAddAdminAndUserAndGuest())
// prepare the content of the DB for testing
// by adding a scenario
scenarioID := addScenario()
configSnapshot1 := json.RawMessage(`{"configs": [ {"Name" : "conf1", "scenarioID" : 1}, {"Name" : "conf2", "scenarioID" : 1}]}`)
confSnapshots := postgres.Jsonb{configSnapshot1}
newResult := ResultRequest{
Description: "This is a test result.",
ScenarioID: scenarioID,
ConfigSnapshots: confSnapshots,
}
// authenticate as normal userB who has no access to new scenario
token, err := helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserBCredentials)
assert.NoError(t, err)
// try to POST with no access
// should result in unprocessable entity
code, resp, err := helper.TestEndpoint(router, token,
base_api_results, "POST", helper.KeyModels{"result": newResult})
assert.NoError(t, err)
assert.Equalf(t, 422, code, "Response body: \n%v\n", resp)
// authenticate as normal user
token, err = helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserACredentials)
assert.NoError(t, err)
// try to POST non JSON body
code, resp, err = helper.TestEndpoint(router, token,
base_api_results, "POST", "this is not JSON")
assert.NoError(t, err)
assert.Equalf(t, 400, code, "Response body: \n%v\n", resp)
// test POST newResult
code, resp, err = helper.TestEndpoint(router, token,
base_api_results, "POST", helper.KeyModels{"result": newResult})
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare POST's response with the newResult
err = helper.CompareResponse(resp, helper.KeyModels{"result": newResult})
assert.NoError(t, err)
// Read newResults's ID from the response
newResultID, err := helper.GetResponseID(resp)
assert.NoError(t, err)
// Get the newResult
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "GET", nil)
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare GET's response with the newResult
err = helper.CompareResponse(resp, helper.KeyModels{"result": newResult})
assert.NoError(t, err)
// try to POST a malformed result
// Required fields are missing
malformedNewResult := ResultRequest{
Description: "ThisIsAMalformedRequest",
}
// this should NOT work and return a unprocessable entity 442 status code
code, resp, err = helper.TestEndpoint(router, token,
base_api_results, "POST", helper.KeyModels{"result": malformedNewResult})
assert.NoError(t, err)
assert.Equalf(t, 422, code, "Response body: \n%v\n", resp)
// authenticate as normal userB who has no access to new scenario
token, err = helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserBCredentials)
assert.NoError(t, err)
// Try to GET the newResult with no access
// Should result in unprocessable entity
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "GET", nil)
assert.NoError(t, err)
assert.Equalf(t, 422, code, "Response body: \n%v\n", resp)
// Test UPDATE/ PUT
updatedResult := ResultRequest{
Description: "This is an updated description",
ConfigSnapshots: confSnapshots,
}
// try to PUT with no access
// should result in unprocessable entity
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "PUT", helper.KeyModels{"result": updatedResult})
assert.NoError(t, err)
assert.Equalf(t, 422, code, "Response body: \n%v\n", resp)
// authenticate as guest user who has access to result
token, err = helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.GuestCredentials)
assert.NoError(t, err)
// try to PUT as guest
// should NOT work and result in unprocessable entity
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "PUT", helper.KeyModels{"result": updatedResult})
assert.NoError(t, err)
assert.Equalf(t, 422, code, "Response body: \n%v\n", resp)
// authenticate as normal user
token, err = helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserACredentials)
assert.NoError(t, err)
// try to PUT a non JSON body
// should result in a bad request
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "PUT", "This is not JSON")
assert.NoError(t, err)
assert.Equalf(t, 400, code, "Response body: \n%v\n", resp)
// test PUT
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "PUT", helper.KeyModels{"result": updatedResult})
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare PUT's response with the updatedResult
err = helper.CompareResponse(resp, helper.KeyModels{"result": updatedResult})
assert.NoError(t, err)
// try to update a result that does not exist (should return not found 404 status code)
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID+1), "PUT", helper.KeyModels{"result": updatedResult})
assert.NoError(t, err)
assert.Equalf(t, 404, code, "Response body: \n%v\n", resp)
// Test DELETE
newResult.Description = updatedResult.Description
// authenticate as normal userB who has no access to new scenario
token, err = helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserBCredentials)
assert.NoError(t, err)
// try to DELETE with no access
// should result in unprocessable entity
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "DELETE", nil)
assert.NoError(t, err)
assert.Equalf(t, 422, code, "Response body: \n%v\n", resp)
// authenticate as normal user
token, err = helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserACredentials)
assert.NoError(t, err)
// Count the number of all the results returned for scenario
initialNumber, err := helper.LengthOfResponse(router, token,
fmt.Sprintf("%v?scenarioID=%v", base_api_results, scenarioID), "GET", nil)
assert.NoError(t, err)
// Delete the added newResult
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "DELETE", nil)
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare DELETE's response with the newResult
err = helper.CompareResponse(resp, helper.KeyModels{"result": newResult})
assert.NoError(t, err)
// Again count the number of all the results returned
finalNumber, err := helper.LengthOfResponse(router, token,
fmt.Sprintf("%v?scenarioID=%v", base_api_results, scenarioID), "GET", nil)
assert.NoError(t, err)
assert.Equal(t, initialNumber-1, finalNumber)
}
func TestAddDeleteResultFile(t *testing.T) {
database.DropTables()
database.MigrateModels()
assert.NoError(t, helper.DBAddAdminAndUserAndGuest())
// prepare the content of the DB for testing
// by adding a scenario
scenarioID := addScenario()
configSnapshot1 := json.RawMessage(`{"configs": [ {"Name" : "conf1", "scenarioID" : 1}, {"Name" : "conf2", "scenarioID" : 1}]}`)
confSnapshots := postgres.Jsonb{configSnapshot1}
newResult := ResultRequest{
Description: "This is a test result.",
ScenarioID: scenarioID,
ConfigSnapshots: confSnapshots,
}
// authenticate as normal user
token, err := helper.AuthenticateForTest(router,
base_api_auth, "POST", helper.UserACredentials)
assert.NoError(t, err)
// test POST newResult
code, resp, err := helper.TestEndpoint(router, token,
base_api_results, "POST", helper.KeyModels{"result": newResult})
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare POST's response with the newResult
err = helper.CompareResponse(resp, helper.KeyModels{"result": newResult})
assert.NoError(t, err)
// Read newResults's ID from the response
newResultID, err := helper.GetResponseID(resp)
assert.NoError(t, err)
// test POST result file
// create a testfile.txt in local folder
c1 := []byte("a,few,values\n1,2,3\n")
err = ioutil.WriteFile("testfile.csv", c1, 0644)
assert.NoError(t, err)
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
fileWriter, err := bodyWriter.CreateFormFile("file", "testuploadfile.csv")
assert.NoError(t, err, "writing to buffer")
// open file handle
fh, err := os.Open("testfile.csv")
assert.NoError(t, err, "opening file")
defer fh.Close()
// io copy
_, err = io.Copy(fileWriter, fh)
assert.NoError(t, err, "IO copy")
contentType := bodyWriter.FormDataContentType()
bodyWriter.Close()
// Create the request
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", fmt.Sprintf("%v/%v/file", base_api_results, newResultID), bodyBuf)
assert.NoError(t, err, "create request")
req.Header.Set("Content-Type", contentType)
req.Header.Add("Authorization", "Bearer "+token)
router.ServeHTTP(w, req)
assert.Equalf(t, 200, w.Code, "Response body: \n%v\n", w.Body)
err = helper.CompareResponse(w.Body, helper.KeyModels{"result": newResult})
// extract file ID from response body
var respResult ResponseResult
err = json.Unmarshal(w.Body.Bytes(), &respResult)
assert.NoError(t, err, "unmarshal response body")
assert.Equal(t, 1, len(respResult.Result.ResultFileIDs))
fileID := respResult.Result.ResultFileIDs[0]
// DELETE the file
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v/file/%v", base_api_results, newResultID, fileID), "DELETE", nil)
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
var respResult2 ResponseResult
err = json.Unmarshal(resp.Bytes(), &respResult2)
assert.NoError(t, err, "unmarshal response body")
assert.Equal(t, 0, len(respResult2.Result.ResultFileIDs))
// ADD the file again
bodyBuf2 := &bytes.Buffer{}
bodyWriter2 := multipart.NewWriter(bodyBuf2)
fileWriter2, err := bodyWriter2.CreateFormFile("file", "testuploadfile.csv")
assert.NoError(t, err, "writing to buffer")
// open file handle
fh2, err := os.Open("testfile.csv")
assert.NoError(t, err, "opening file")
defer fh2.Close()
// io copy
_, err = io.Copy(fileWriter2, fh2)
assert.NoError(t, err, "IO copy")
contentType2 := bodyWriter2.FormDataContentType()
bodyWriter2.Close()
// Create the request
w2 := httptest.NewRecorder()
req2, err := http.NewRequest("POST", fmt.Sprintf("%v/%v/file", base_api_results, newResultID), bodyBuf2)
assert.NoError(t, err, "create request")
req2.Header.Set("Content-Type", contentType2)
req2.Header.Add("Authorization", "Bearer "+token)
router.ServeHTTP(w2, req2)
assert.Equalf(t, 200, w2.Code, "Response body: \n%v\n", w2.Body)
err = helper.CompareResponse(w2.Body, helper.KeyModels{"result": newResult})
// extract file ID from response body
var respResult3 ResponseResult
err = json.Unmarshal(w2.Body.Bytes(), &respResult3)
assert.NoError(t, err, "unmarshal response body")
assert.Equal(t, 1, len(respResult3.Result.ResultFileIDs))
// DELETE result inlc. file
code, resp, err = helper.TestEndpoint(router, token,
fmt.Sprintf("%v/%v", base_api_results, newResultID), "DELETE", nil)
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare DELETE's response with the newResult
err = helper.CompareResponse(resp, helper.KeyModels{"result": newResult})
assert.NoError(t, err)
// Again count the number of all the results returned
finalNumber, err := helper.LengthOfResponse(router, token,
fmt.Sprintf("%v?scenarioID=%v", base_api_results, scenarioID), "GET", nil)
assert.NoError(t, err)
assert.Equal(t, 0, finalNumber)
}

View file

@ -0,0 +1,97 @@
/** Result package, validators.
*
* @author Sonja Happ <sonja.happ@eonerc.rwth-aachen.de>
* @copyright 2014-2019, Institute for Automation of Complex Power Systems, EONERC
* @license GNU General Public License (version 3)
*
* VILLASweb-backend-go
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
package result
import (
"encoding/json"
"github.com/jinzhu/gorm/dialects/postgres"
"github.com/nsf/jsondiff"
"gopkg.in/go-playground/validator.v9"
)
var validate *validator.Validate
type validNewResult struct {
Description string `form:"Description" validate:"omitempty"`
ResultFileIDs []int64 `form:"ResultFileIDs" validate:"omitempty"`
ConfigSnapshots postgres.Jsonb `form:"ConfigSnapshots" validate:"required"`
ScenarioID uint `form:"ScenarioID" validate:"required"`
}
type validUpdatedResult struct {
Description string `form:"Description" validate:"omitempty" json:"description"`
ResultFileIDs []int64 `form:"ResultFileIDs" validate:"omitempty" json:"resultFileIDs"`
ConfigSnapshots postgres.Jsonb `form:"ConfigSnapshots" validate:"omitempty" json:"configSnapshots"`
}
type addResultRequest struct {
Result validNewResult `json:"result"`
}
type updateResultRequest struct {
Result validUpdatedResult `json:"result"`
}
func (r *addResultRequest) validate() error {
validate = validator.New()
errs := validate.Struct(r)
return errs
}
func (r *validUpdatedResult) validate() error {
validate = validator.New()
errs := validate.Struct(r)
return errs
}
func (r *addResultRequest) createResult() Result {
var s Result
s.Description = r.Result.Description
s.ConfigSnapshots = r.Result.ConfigSnapshots
s.ResultFileIDs = r.Result.ResultFileIDs
s.ScenarioID = r.Result.ScenarioID
return s
}
func (r *updateResultRequest) updatedResult(oldResult Result) Result {
// Use the old Result as a basis for the updated Result `s`
s := oldResult
s.Result.Description = r.Result.Description
s.ResultFileIDs = r.Result.ResultFileIDs
// only update snapshots if not empty
var emptyJson postgres.Jsonb
// Serialize empty json and params
emptyJson_ser, _ := json.Marshal(emptyJson)
configSnapshots_ser, _ := json.Marshal(r.Result.ConfigSnapshots)
opts := jsondiff.DefaultConsoleOptions()
diff, _ := jsondiff.Compare(emptyJson_ser, configSnapshots_ser, &opts)
if diff.String() != "FullMatch" {
s.ConfigSnapshots = r.Result.ConfigSnapshots
}
return s
}

View file

@ -59,7 +59,6 @@ func getScenarios(c *gin.Context) {
// ATTENTION: do not use c.GetInt (common.UserIDCtx) since user_id is of type uint and not int
userID, _ := c.Get(database.UserIDCtx)
userRole, _ := c.Get(database.UserRoleCtx)
var u user.User
err := u.ByID(userID.(uint))
@ -70,7 +69,7 @@ func getScenarios(c *gin.Context) {
// get all scenarios for the user who issues the request
db := database.GetDB()
var scenarios []database.Scenario
if userRole == "Admin" { // Admin can see all scenarios
if u.Role == "Admin" { // Admin can see all scenarios
err = db.Order("ID asc").Find(&scenarios).Error
if helper.DBError(c, err) {
return

View file

@ -500,7 +500,12 @@ func TestAddUserToScenario(t *testing.T) {
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare resp to userB
err = helper.CompareResponse(resp, helper.KeyModels{"user": helper.UserB})
userB := UserRequest{
Username: helper.UserB.Username,
Mail: helper.UserB.Mail,
Role: helper.UserB.Role,
}
err = helper.CompareResponse(resp, helper.KeyModels{"user": userB})
assert.NoError(t, err)
// Count AGAIN the number of all the users returned for newScenario
@ -663,8 +668,13 @@ func TestRemoveUserFromScenario(t *testing.T) {
assert.NoError(t, err)
assert.Equalf(t, 200, code, "Response body: \n%v\n", resp)
// Compare DELETE's response with UserB
err = helper.CompareResponse(resp, helper.KeyModels{"user": helper.UserC})
// Compare DELETE's response with UserC's data
userC := UserRequest{
Username: helper.UserC.Username,
Mail: helper.UserC.Mail,
Role: helper.UserC.Role,
}
err = helper.CompareResponse(resp, helper.KeyModels{"user": userC})
assert.NoError(t, err)
// Count AGAIN the number of all the users returned for newScenario

View file

@ -107,7 +107,6 @@ func (s *Signal) delete() error {
}
// remove association between Signal and ComponentConfiguration
// Signal itself is not deleted from DB, it remains as "dangling"
if s.Direction == "in" {
err = db.Model(&m).Association("InputMapping").Delete(s).Error
if err != nil {
@ -117,6 +116,9 @@ func (s *Signal) delete() error {
// Reduce length of mapping by 1
var newInputLength = m.InputLength - 1
err = db.Model(m).Update("InputLength", newInputLength).Error
if err != nil {
return err
}
} else {
err = db.Model(&m).Association("OutputMapping").Delete(s).Error
@ -127,7 +129,13 @@ func (s *Signal) delete() error {
// Reduce length of mapping by 1
var newOutputLength = m.OutputLength - 1
err = db.Model(m).Update("OutputLength", newOutputLength).Error
if err != nil {
return err
}
}
// Delete signal
err = db.Delete(s).Error
return err
}

View file

@ -96,10 +96,13 @@ func (w *Widget) delete() error {
}
// remove association between Dashboard and Widget
// Widget itself is not deleted from DB, it remains as "dangling"
err = db.Model(&dab).Association("Widgets").Delete(w).Error
if err != nil {
return err
}
// TODO: What about files that belong to a widget? Keep them or remove them here?
// Delete Widget
err = db.Delete(w).Error
return err
}