From b29e046b72b07f0411d6a0aa714e989c65f80064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Fri, 29 Dec 2023 21:08:50 -0500 Subject: [PATCH] release: v0.37.0 (#128) * docs(openapi): Update spec (#121) * feat: List supported languages (#122) * feat: Download languages template (#123) * feat: Create new test block (#126) * feat: Update test block (#127) --- CHANGELOG.md | 21 +- __tests__/data/java_tests_sample.zip | Bin 0 -> 5178 bytes __tests__/integration/blocks_test.go | 75 ++++ __tests__/integration/blocks_utils_test.go | 62 ++- .../integration/laboratories_utils_test.go | 67 +++- __tests__/integration/laboratorires_test.go | 47 +++ __tests__/integration/languages_test.go | 76 ++++ __tests__/integration/languages_utils_test.go | 21 + __tests__/integration/main_test.go | 21 + docker-compose.yaml | 12 + .../blocks/update-markdown-block-content.bru | 2 +- docs/bruno/languages/download-template.bru | 11 + docs/bruno/languages/list-languages.bru | 11 + docs/insomnia/collection.json | 206 ++++++++++ docs/openapi/spec.openapi.yaml | 371 +++++++++++++++++- go.mod | 2 +- go.sum | 30 -- sql/migrations/20230920232901_init.down.sql | 2 + sql/migrations/20230920232901_init.up.sql | 70 +++- src/blocks/application/use_cases.go | 40 +- .../domain/definitions/blocks_repository.go | 22 ++ src/blocks/domain/dtos/blocks_dtos.go | 10 + ...oes_not_owns_block.go => blocks_errors.go} | 10 + src/blocks/infrastructure/http/controllers.go | 73 +++- src/blocks/infrastructure/http/routes.go | 19 +- .../implementations/blocks_repository.go | 259 +++++++++++- .../requests/blocks_requests.go | 5 + src/config/infrastructure/http_server.go | 2 + src/laboratories/application/use_cases.go | 38 +- .../definitions/laboratories_repository.go | 1 + .../domain/dtos/laboratories_dtos.go | 11 + .../infrastructure/http/controllers.go | 66 ++++ .../infrastructure/http/routes.go | 18 +- .../laboratories_respository.go | 72 +++- .../requests/laboratories_requests.go | 6 + src/languages/application/usec_cases.go | 25 ++ .../definitions/languages_repository.go | 11 + src/languages/domain/dtos/.gitkeep | 0 src/languages/domain/entities/language.go | 7 + .../domain/errors/languages_errors.go | 13 + .../infrastructure/http/http_controllers.go | 48 +++ .../infrastructure/http/http_routes.go | 33 ++ .../implementations/languages_repository.go | 140 +++++++ .../infrastructure/requests/.gitkeep | 0 src/shared/domain/errors/errors.go | 13 + src/shared/infrastructure/environment.go | 19 +- src/shared/infrastructure/utils.go | 95 ++++- version.json | 2 +- 48 files changed, 2065 insertions(+), 100 deletions(-) create mode 100644 __tests__/data/java_tests_sample.zip create mode 100644 __tests__/integration/languages_test.go create mode 100644 __tests__/integration/languages_utils_test.go create mode 100644 docs/bruno/languages/download-template.bru create mode 100644 docs/bruno/languages/list-languages.bru create mode 100644 docs/insomnia/collection.json rename src/blocks/domain/errors/{teacher_does_not_owns_block.go => blocks_errors.go} (56%) create mode 100644 src/languages/application/usec_cases.go create mode 100644 src/languages/domain/definitions/languages_repository.go create mode 100644 src/languages/domain/dtos/.gitkeep create mode 100644 src/languages/domain/entities/language.go create mode 100644 src/languages/domain/errors/languages_errors.go create mode 100644 src/languages/infrastructure/http/http_controllers.go create mode 100644 src/languages/infrastructure/http/http_routes.go create mode 100644 src/languages/infrastructure/implementations/languages_repository.go create mode 100644 src/languages/infrastructure/requests/.gitkeep diff --git a/CHANGELOG.md b/CHANGELOG.md index 556b766..9c01fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,45 @@ -# [0.33.0](https://github.com/upb-code-labs/main-api/compare/v0.32.0...v0.33.0) (2023-11-29) +# [0.37.0](https://github.com/upb-code-labs/main-api/compare/v0.36.0...v0.37.0) (2023-12-30) ### Features -* Update markdown block content ([#113](https://github.com/upb-code-labs/main-api/issues/113)) ([3a6f762](https://github.com/upb-code-labs/main-api/commit/3a6f762a1c5e1e3915bee380438803b0ce981aac)) +* Update test block ([#127](https://github.com/upb-code-labs/main-api/issues/127)) ([fa8c816](https://github.com/upb-code-labs/main-api/commit/fa8c816083650039ae831daeff69c8e65689796a)) -# [0.32.0](https://github.com/upb-code-labs/main-api/compare/v0.31.0...v0.32.0) (2023-11-28) +# [0.36.0](https://github.com/upb-code-labs/main-api/compare/v0.35.0...v0.36.0) (2023-12-29) ### Features -* Create empty markdown block ([#112](https://github.com/upb-code-labs/main-api/issues/112)) ([e3c5ef1](https://github.com/upb-code-labs/main-api/commit/e3c5ef1f54b3e76d36bcb1e6d6f54f3da2da4fa9)) +* Create new test block ([#126](https://github.com/upb-code-labs/main-api/issues/126)) ([ae8c2ba](https://github.com/upb-code-labs/main-api/commit/ae8c2ba50914ec235cecb0a41784736f2a4692ac)) -# [0.31.0](https://github.com/upb-code-labs/main-api/compare/v0.30.0...v0.31.0) (2023-11-28) +# [0.35.0](https://github.com/upb-code-labs/main-api/compare/v0.34.0...v0.35.0) (2023-12-28) ### Features -* Get course laboratories ([#111](https://github.com/upb-code-labs/main-api/issues/111)) ([653473e](https://github.com/upb-code-labs/main-api/commit/653473e1d2a960267c1a960dd5fc616868383cd9)) -* Get laboratory by UUID ([#110](https://github.com/upb-code-labs/main-api/issues/110)) ([44a870d](https://github.com/upb-code-labs/main-api/commit/44a870ddc5b9da417cc5bb8ccd660f7d6df8681f)) +* Download languages template ([#123](https://github.com/upb-code-labs/main-api/issues/123)) ([4f460f4](https://github.com/upb-code-labs/main-api/commit/4f460f41a90f38516b6dad38f40a97f5245efb99)) -# [0.30.0](https://github.com/upb-code-labs/main-api/compare/v0.29.0...v0.30.0) (2023-11-28) +# [0.34.0](https://github.com/upb-code-labs/main-api/compare/v0.33.0...v0.34.0) (2023-12-28) ### Features -* Update laboratory by UUID ([#109](https://github.com/upb-code-labs/main-api/issues/109)) ([e388a54](https://github.com/upb-code-labs/main-api/commit/e388a5439241143c9e93e9f488b7f6bc9b5618cd)) +* List supported languages ([#122](https://github.com/upb-code-labs/main-api/issues/122)) ([e7d7539](https://github.com/upb-code-labs/main-api/commit/e7d75396b242cdebc046d62ca184522bcfbb2ae2)) -# [0.29.0](https://github.com/upb-code-labs/main-api/compare/v0.28.0...v0.29.0) (2023-11-27) +# [0.33.0](https://github.com/upb-code-labs/main-api/compare/v0.32.0...v0.33.0) (2023-11-29) ### Features -* Create laboratory ([#103](https://github.com/upb-code-labs/main-api/issues/103)) ([c577ea2](https://github.com/upb-code-labs/main-api/commit/c577ea29c9904da5943a96a2e421f6d5301d6866)) +* Update markdown block content ([#113](https://github.com/upb-code-labs/main-api/issues/113)) ([3a6f762](https://github.com/upb-code-labs/main-api/commit/3a6f762a1c5e1e3915bee380438803b0ce981aac)) diff --git a/__tests__/data/java_tests_sample.zip b/__tests__/data/java_tests_sample.zip new file mode 100644 index 0000000000000000000000000000000000000000..a7e3d11fa7e947d91761d2e3e134a4854111a5f1 GIT binary patch literal 5178 zcmb`K2|UyNAHe6B7$Fiuj^r$7$rY&_k)v4da~sCy+6?7ha)&vda$h}4ipMdk$rYlk z6j4buxq3#W$713?y6owBx}N`Uul-)XyckRg00v9Oz5s8*a_M{z&yzW0Ic0C{`GbCPp>P$p|I~mHq)fCuIaF|!s)mM z0{~#l2mtWXLy&M3L?IZ7_?AYu;UCGJzR+A%PHAb;OM@D9HbL&k*t`|sY8G=cgN zvLI!z<^vOWFJzREeKDAh-gj9h$D(DK%g=Hy`esBLEAh!CPElNksfDDY()U>uQwOCN z%nU^i6f>ieK^Z#|Cb%Cw>WMz;FtclGehYKIEtKf@Qs4W^QszaDcZUybwe%LYXtk5R z9Kz;Bt=i=U*FUi(aF1f7=OHP=3kM?^#!*38kiBR=4rem4=PB8zCfAg+bINT1wsr1C zr8weEY)zTA8y7N%!jJ6^qu+9C*?GU$CgyGOYAr%Zu#BsmhLK)}5n~xXr|&!}j(uE7Okmy%qaP^}uDI@UXfdvh#0*G3r#g?4T`q zr+HvvHfU zJC@Quzoc)?akBr=`M1#vM7{dL&GY*j1`8 z#qqp-^dzEq)ang@-qz+$pdT~M&MmZf=cl(7;*0d7IW6FOrpBZxo8MI8h@nvVj?Qlv zQ&y^N95q9*qUBUUm+fCLq)b`A z3Lv_;`&I;B2HX2SHrgD1xAGNn?y*A~3}|&L>LxCVRqLr7IELk}ctes0L6uZ{VLIX& z1Y&ex|7a0c)}N%=p{9l3DN`P;mpv>rvx?2V%rb4nB)Z&Q_epb&4$h>SV=pSVx85W2*nvQwAw^V-IH#+W5i|J&FsR#(hKBFQ`s|V(<;+C-n;`gss?o1XPIB5 zA+WH0?^~UeIIwK1HRo${chrEDc9f{MYEiUK)MIIqXSG0uXNPHlK&I=Nv#$LWn|i$( z;q_C2Jw8r*FU{ge2msjaakUf>* z5mdDXr7NA7>*WuKsd&|zC7yjm>Gb22rq0*3_%MPfm3rXrp?AVsQG33wQw7~{}8HtAqb_y1>m+au*cqF~;k{NkX`#6d5nMNNa z*m2q{%~8&f$uP);cq8=MSSmGR#S$oQaUrBIk52)tb>4?Ni9(fl&f;BeT-~NOdqH`( z9TRHc+~8#1rXHK&<@Yg;J7R9sBs&_iVZ79M4iOd|>au$o{8seh%X@+m8VvQMN`^r0#0Yh3_m?0bdG=>`qLn| z%J=A-2N|84dYX`&cbnt?Dim+F5_TeCCT_{d?VbTnEY5q1{I=~_qXtg|Rw{8E9Xs&Y z`Qf3Rc{>iLmDk!_WztAekwZ5%=q-L%Hq8q zCt;E`_$XgIcAH$;sF8be2r6Mh<;1YO$)k&Q^W(U~3ohM38|4O~co_#SWf-Dg>C)$H zLipxUPFf)}rRD85`apGWM5w`_>PmkMDF*=*uBz;bZm zgr*A7DZCvYk;Dci$ccK^NtC8~3fons*|-*#YAFf0Zs5cwO!A%DN-p!l@hekLSmGC) z@bl?2Dt&Wo*Ua`}EH@b26UrAjB8S@cf~luOp|5_+ry`AA=5|w!C=(#@kpo9N1rDeDAG+353L7 z&_%9s7s$b+GySa^K!x6prPJ1j^T9Lso#eYBPR2&xMHQ^Di0T^t1zh#=KaY59pS+9; zfc)Z^$GIMXbcOr;BIk3LUsYv|697Z`1_nT2s9z+0o*h3E|HCv(XC@kkLjOOMTI2kX zQeP$`h7&mhv$V;mkd`Q1pM3vg6PaUbi&vWx$LZPdgp47^KNQ2FkmMpPGPe1O{vUOH z?fyv>zyC=tlT=7@x>Gs360=kJUL>N=ECsbYC^{qc4y$Sh-f9t4iZO+#*!qX=E?(%h zR)>tQIP?cP=1D+Umm4)j&M0PbpKm;9kvvZ!5Fj?ghwPU;vF3vHVv;&s0%?)GvPh<} z+(<>^mB<(I;q?-|X#Ly99?*IJTg5!_!TNC~Eg{eBs`!W8LnSJSnm2FWcq=q@4u-R} ziI{7N4{*ZR?cbz1Pu@Qgebab+VW;^!|IFwC(a@m&Cu7}ND133^90PuFT8(rsPwNrS z%HO7}e7;bfc$&}hX~l)(li&P&k>4Gyj_LV4QQ{_0`J!he&PxJ8SohR}iOfdX05j}x z!k)ULo<@>-$M96DO6g7$gZtBBMm4HqL}K;Jg9GkbwPs#*-SWrA1?7soFJ^<6=N3-B z-qSSB?p`rm{L0ovzP|ezVXuj?)?kr@%YK4*FTTh;h3joX#%P8drumNhf&|%ne-VpFuLhtQA{FL*}RcQdq5Mj)Suc%Efu@HC3Tk> zbK&(?(gH5zX0&;o62FDt#<{Fk!JQ&>b5d8Miz{!du|+ZqSmdF%x4It~SK7Gt8;D^QatEKhdQS5Xm-A|}bOYFCRUy_u5h5eu# zE%pGVQK#sEUl!VLfxj3{zm=^AgJ|2sn$aILU0)Hth5n*5{mQi-9Y>G;+XD7ubowP{ zJsnL;$aRX-uRK2nryus$!@HR2;C~zae~eB)maM0T(UyyKs{iAV@+}!(&XpDwpD#AL z2qdw6l<}cO|2&S94ktyExu){xrU^DG L0Ki4g)nESu>(@bn literal 0 HcmV?d00001 diff --git a/__tests__/integration/blocks_test.go b/__tests__/integration/blocks_test.go index 54499e4..4049025 100644 --- a/__tests__/integration/blocks_test.go +++ b/__tests__/integration/blocks_test.go @@ -67,3 +67,78 @@ func TestUpdateMarkdownBlockContent(t *testing.T) { block := response["markdown_blocks"].([]interface{})[0].(map[string]interface{}) c.Equal("# Updated main title", block["content"].(string)) } + +func TestUpdateTestBlock(t *testing.T) { + c := require.New(t) + + // Login as a teacher + w, r := PrepareRequest("POST", "/api/v1/session/login", map[string]interface{}{ + "email": registeredTeacherEmail, + "password": registeredTeacherPass, + }) + router.ServeHTTP(w, r) + cookie := w.Result().Cookies()[0] + + // Create a course + courseUUID, status := CreateCourse("Update test block test - course") + c.Equal(http.StatusCreated, status) + + // Create a laboratory + laboratoryCreationResponse, status := CreateLaboratory(cookie, map[string]interface{}{ + "name": "Update test block test - laboratory", + "course_uuid": courseUUID, + "opening_date": "2023-12-01T08:00", + "due_date": "3023-12-01T00:00", + }) + c.Equal(http.StatusCreated, status) + laboratoryUUID := laboratoryCreationResponse["uuid"].(string) + + // Get the supported languages + languagesResponse, status := GetSupportedLanguages(cookie) + c.Equal(http.StatusOK, status) + + languages := languagesResponse["languages"].([]interface{}) + c.Greater(len(languages), 0) + + firstLanguage := languages[0].(map[string]interface{}) + firstLanguageUUID := firstLanguage["uuid"].(string) + + // Create a test block + zipFile, err := GetSampleTestsArchive() + c.Nil(err) + + blockCreationResponse, status := CreateTestBlock(&CreateTestBlockUtilsDTO{ + laboratoryUUID: laboratoryUUID, + languageUUID: firstLanguageUUID, + blockName: "Update test block test - block", + cookie: cookie, + testFile: zipFile, + }) + c.Equal(http.StatusCreated, status) + testBlockUUID := blockCreationResponse["uuid"].(string) + + // Update the test block + newName := "Update test block test - block - updated" + zipFile, err = GetSampleTestsArchive() + c.Nil(err) + + _, status = UpdateTestBlock(&UpdateTestBlockUtilsDTO{ + blockUUID: testBlockUUID, + languageUUID: firstLanguageUUID, + blockName: newName, + cookie: cookie, + testFile: zipFile, + }) + c.Equal(http.StatusNoContent, status) + + // Check that the test block data was updated + laboratoryResponse, status := GetLaboratoryByUUID(cookie, laboratoryUUID) + c.Equal(http.StatusOK, status) + + blocks := laboratoryResponse["test_blocks"].([]interface{}) + c.Equal(1, len(blocks)) + + block := blocks[0].(map[string]interface{}) + c.Equal(newName, block["name"].(string)) + c.Equal(firstLanguageUUID, block["language_uuid"].(string)) +} diff --git a/__tests__/integration/blocks_utils_test.go b/__tests__/integration/blocks_utils_test.go index 723dc93..22a040d 100644 --- a/__tests__/integration/blocks_utils_test.go +++ b/__tests__/integration/blocks_utils_test.go @@ -1,16 +1,76 @@ package integration import ( + "bytes" "fmt" + "io" + "mime/multipart" "net/http" + "net/http/httptest" + "os" ) func UpdateMarkdownBlockContent(cookie *http.Cookie, blockUUID string, payload map[string]interface{}) (response map[string]interface{}, statusCode int) { endpoint := fmt.Sprintf("/api/v1/blocks/markdown_blocks/%s/content", blockUUID) - w, r := PrepareRequest("PUT", endpoint, payload) + w, r := PrepareRequest("PATCH", endpoint, payload) r.AddCookie(cookie) router.ServeHTTP(w, r) jsonResponse := ParseJsonResponse(w.Body) return jsonResponse, w.Code } + +type UpdateTestBlockUtilsDTO struct { + blockUUID string + languageUUID string + blockName string + cookie *http.Cookie + testFile *os.File +} + +func UpdateTestBlock(dto *UpdateTestBlockUtilsDTO) (response map[string]interface{}, statusCode int) { + // Create the request body + var body bytes.Buffer + + // Create the multipart form + writer := multipart.NewWriter(&body) + + // Add the block name + _ = writer.WriteField("block_name", dto.blockName) + + // Add the language UUID + _ = writer.WriteField("language_uuid", dto.languageUUID) + + // Add the test file + part, err := writer.CreateFormFile("test_archive", dto.testFile.Name()) + if err != nil { + panic(err) + } + _, err = io.Copy(part, dto.testFile) + if err != nil { + panic(err) + } + + // Close the multipart form + err = writer.Close() + if err != nil { + panic(err) + } + + // Create the request + endpoint := fmt.Sprintf("/api/v1/blocks/test_blocks/%s", dto.blockUUID) + r, err := http.NewRequest("PUT", endpoint, &body) + if err != nil { + panic(err) + } + r.Header.Add("Content-Type", writer.FormDataContentType()) + r.AddCookie(dto.cookie) + + // Send the request + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + // Parse the response + jsonResponse := ParseJsonResponse(w.Body) + return jsonResponse, w.Code +} diff --git a/__tests__/integration/laboratories_utils_test.go b/__tests__/integration/laboratories_utils_test.go index 988ad96..d1323b7 100644 --- a/__tests__/integration/laboratories_utils_test.go +++ b/__tests__/integration/laboratories_utils_test.go @@ -1,6 +1,13 @@ package integration -import "net/http" +import ( + "bytes" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "os" +) func CreateLaboratory(cookie *http.Cookie, payload map[string]interface{}) (response map[string]interface{}, statusCode int) { w, r := PrepareRequest("POST", "/api/v1/laboratories", payload) @@ -37,3 +44,61 @@ func CreateMarkdownBlock(cookie *http.Cookie, laboratoryUUID string) (response m jsonResponse := ParseJsonResponse(w.Body) return jsonResponse, w.Code } + +type CreateTestBlockUtilsDTO struct { + laboratoryUUID string + languageUUID string + blockName string + cookie *http.Cookie + testFile *os.File +} + +func CreateTestBlock(dto *CreateTestBlockUtilsDTO) (response map[string]interface{}, statusCode int) { + // Create the request body + var body bytes.Buffer + + // Create the multipart form + writer := multipart.NewWriter(&body) + + // Add the file to the form + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", "form-data; name=\"test_archive\"; filename=\"test.zip\"") + h.Set("Content-Type", "application/zip") + + fileWriter, err := writer.CreatePart(h) + if err != nil { + panic(err) + } + + _, err = io.Copy(fileWriter, dto.testFile) + if err != nil { + panic(err) + } + + // Add the text fields to the form + err = writer.WriteField("block_name", dto.blockName) + if err != nil { + panic(err) + } + + err = writer.WriteField("language_uuid", dto.languageUUID) + if err != nil { + panic(err) + } + + // Close the multipart form + err = writer.Close() + if err != nil { + panic(err) + } + + // Create the request + w, r := PrepareMultipartRequest("POST", "/api/v1/laboratories/test_blocks/"+dto.laboratoryUUID, &body) + r.AddCookie(dto.cookie) + r.Header.Set("Content-Type", writer.FormDataContentType()) + + // Send the request + router.ServeHTTP(w, r) + jsonResponse := ParseJsonResponse(w.Body) + return jsonResponse, w.Code +} diff --git a/__tests__/integration/laboratorires_test.go b/__tests__/integration/laboratorires_test.go index 4bb600c..e5656f4 100644 --- a/__tests__/integration/laboratorires_test.go +++ b/__tests__/integration/laboratorires_test.go @@ -279,3 +279,50 @@ func TestCreateMarkdownBlock(t *testing.T) { c.Equal("", block["content"]) c.EqualValues(1, block["index"]) } + +func TestCreateTestBlock(t *testing.T) { + c := require.New(t) + + // Login as a teacher + w, r := PrepareRequest("POST", "/api/v1/session/login", map[string]interface{}{ + "email": registeredTeacherEmail, + "password": registeredTeacherPass, + }) + router.ServeHTTP(w, r) + cookie := w.Result().Cookies()[0] + + // Create a course + courseUUID, status := CreateCourse("Create test block test - course") + c.Equal(http.StatusCreated, status) + + // Create a laboratory + laboratoryCreationResponse, status := CreateLaboratory(cookie, map[string]interface{}{ + "name": "Create test block test - laboratory", + "course_uuid": courseUUID, + "opening_date": "2023-12-01T08:00", + "due_date": "3023-12-01T00:00", + }) + laboratoryUUID := laboratoryCreationResponse["uuid"].(string) + c.Equal(http.StatusCreated, status) + + // Get a supported language from the supported languages list + language := GetFirstSupportedLanguage(cookie) + languageUUID := language["uuid"].(string) + + // Open `.zip` file from the data folder + zipFile, err := GetSampleTestsArchive() + c.Nil(err) + + // Send the request + response, _ := CreateTestBlock(&CreateTestBlockUtilsDTO{ + laboratoryUUID: laboratoryUUID, + languageUUID: languageUUID, + blockName: "Create test block test - block", + cookie: cookie, + testFile: zipFile, + }) + + // Validate the response + // c.Equal(http.StatusCreated, status) + c.Contains(response, "uuid") +} diff --git a/__tests__/integration/languages_test.go b/__tests__/integration/languages_test.go new file mode 100644 index 0000000..cc6091d --- /dev/null +++ b/__tests__/integration/languages_test.go @@ -0,0 +1,76 @@ +package integration + +import ( + "net/http" + "testing" + + "github.com/gabriel-vasile/mimetype" + "github.com/stretchr/testify/require" +) + +func TestListSupportedLanguages(t *testing.T) { + c := require.New(t) + + // Login as an student + w, r := PrepareRequest("POST", "/api/v1/session/login", map[string]interface{}{ + "email": registeredStudentEmail, + "password": registeredStudentPass, + }) + router.ServeHTTP(w, r) + cookie := w.Result().Cookies()[0] + + // Get the supported languages + response, statusCode := GetSupportedLanguages(cookie) + c.Equal(http.StatusOK, statusCode) + + // Check the response + languages := response["languages"].([]interface{}) + c.Greater(len(languages), 0) + + // Get all the languages names + var languagesNames []string + for _, language := range languages { + languagesNames = append(languagesNames, language.(map[string]interface{})["name"].(string)) + } + + // Check if the supported languages are included + c.Contains(languagesNames, "Java") +} + +func GetFirstSupportedLanguage(cookie *http.Cookie) (language map[string]interface{}) { + w, r := PrepareRequest("GET", "/api/v1/languages", nil) + r.AddCookie(cookie) + router.ServeHTTP(w, r) + + jsonResponse := ParseJsonResponse(w.Body) + + languages := jsonResponse["languages"].([]interface{}) + language = languages[0].(map[string]interface{}) + return language +} + +func TestGetLanguageTemplate(t *testing.T) { + c := require.New(t) + + // Login as an student + w, r := PrepareRequest("POST", "/api/v1/session/login", map[string]interface{}{ + "email": registeredStudentEmail, + "password": registeredStudentPass, + }) + router.ServeHTTP(w, r) + cookie := w.Result().Cookies()[0] + + // Get a supported language from the supported languages list + language := GetFirstSupportedLanguage(cookie) + + // Get the language template + template, statusCode := GetLanguageTemplate(cookie, language["uuid"].(string)) + c.Equal(http.StatusOK, statusCode) + + // Check the response + c.Greater(len(template), 0) + + // Check the MIMETYPE + mtype := mimetype.Detect(template) + c.Equal("application/zip", mtype.String()) +} diff --git a/__tests__/integration/languages_utils_test.go b/__tests__/integration/languages_utils_test.go new file mode 100644 index 0000000..a73087c --- /dev/null +++ b/__tests__/integration/languages_utils_test.go @@ -0,0 +1,21 @@ +package integration + +import ( + "fmt" + "net/http" +) + +func GetSupportedLanguages(cookie *http.Cookie) (response map[string]interface{}, statusCode int) { + w, r := PrepareRequest("GET", "/api/v1/languages", nil) + r.AddCookie(cookie) + router.ServeHTTP(w, r) + return ParseJsonResponse(w.Body), w.Code +} + +func GetLanguageTemplate(cookie *http.Cookie, uuid string) (bytes []byte, statusCode int) { + endpoint := fmt.Sprintf("/api/v1/languages/%s/template", uuid) + w, r := PrepareRequest("GET", endpoint, nil) + r.AddCookie(cookie) + router.ServeHTTP(w, r) + return w.Body.Bytes(), w.Code +} diff --git a/__tests__/integration/main_test.go b/__tests__/integration/main_test.go index 5b265f0..7e909c7 100644 --- a/__tests__/integration/main_test.go +++ b/__tests__/integration/main_test.go @@ -118,6 +118,17 @@ func registerBaseTeachers() { } // --- Helpers --- +func GetSampleTestsArchive() (*os.File, error) { + TEST_FILE_PATH := "../data/java_tests_sample.zip" + + zipFile, err := os.Open(TEST_FILE_PATH) + if err != nil { + return nil, err + } + + return zipFile, nil +} + func PrepareRequest(method, endpoint string, payload interface{}) (*httptest.ResponseRecorder, *http.Request) { var req *http.Request @@ -133,6 +144,16 @@ func PrepareRequest(method, endpoint string, payload interface{}) (*httptest.Res return w, req } +func PrepareMultipartRequest(method, endpoint string, body *bytes.Buffer) (*httptest.ResponseRecorder, *http.Request) { + var req *http.Request + + req, _ = http.NewRequest(method, endpoint, body) + req.Header.Set("Content-Type", "multipart/form-data") + + w := httptest.NewRecorder() + return w, req +} + func ParseJsonResponse(buffer *bytes.Buffer) map[string]interface{} { var response map[string]interface{} json.Unmarshal(buffer.Bytes(), &response) diff --git a/docker-compose.yaml b/docker-compose.yaml index be6285b..24369d4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,7 @@ version: '3.8' services: + # Databases codelabs_postgres_db: image: docker.io/library/postgres:alpine3.18 container_name: codelabs_postgres_db @@ -14,6 +15,17 @@ services: volumes: - ./volumes/postgres:/var/lib/postgresql/data + # Microservices + codelabs_static_files: + image: ghcr.io/upb-code-labs/static-files-microservice:latest + container_name: codelabs_static_files + restart: on-failure + ports: + - "127.0.0.1:8081:8080" + depends_on: + - codelabs_postgres_db + + # Utils postgres_db_admin: image: docker.io/dpage/pgadmin4:7.6 container_name: postgres_db_admin diff --git a/docs/bruno/blocks/update-markdown-block-content.bru b/docs/bruno/blocks/update-markdown-block-content.bru index fc6130e..387f4fc 100644 --- a/docs/bruno/blocks/update-markdown-block-content.bru +++ b/docs/bruno/blocks/update-markdown-block-content.bru @@ -4,7 +4,7 @@ meta { seq: 1 } -put { +patch { url: {{BASE_URL}}/blocks/markdown_blocks/eb4ef0ee-3865-44c3-8d52-2620fb2653e5/content body: json auth: none diff --git a/docs/bruno/languages/download-template.bru b/docs/bruno/languages/download-template.bru new file mode 100644 index 0000000..3508e3f --- /dev/null +++ b/docs/bruno/languages/download-template.bru @@ -0,0 +1,11 @@ +meta { + name: download-template + type: http + seq: 2 +} + +get { + url: {{BASE_URL}}/languages/19c11b28-3d06-4c54-8946-6af8a28e8b07/template + body: none + auth: none +} diff --git a/docs/bruno/languages/list-languages.bru b/docs/bruno/languages/list-languages.bru new file mode 100644 index 0000000..1e6322e --- /dev/null +++ b/docs/bruno/languages/list-languages.bru @@ -0,0 +1,11 @@ +meta { + name: list-languages + type: http + seq: 1 +} + +get { + url: {{BASE_URL}}/languages + body: none + auth: none +} diff --git a/docs/insomnia/collection.json b/docs/insomnia/collection.json new file mode 100644 index 0000000..ae3bd29 --- /dev/null +++ b/docs/insomnia/collection.json @@ -0,0 +1,206 @@ +{ + "_type": "export", + "__export_format": 4, + "__export_date": "2023-12-30T01:37:22.228Z", + "__export_source": "insomnia.desktop.app:v8.5.1", + "resources": [ + { + "_id": "req_415eff49d69146239cb94e4840c73722", + "parentId": "fld_0f115af031f64a478dc8707d5aba1048", + "modified": 1703900064178, + "created": 1703898807668, + "url": "{{ _.BASE_URL }}/blocks/test_blocks/dac2c142-d38b-4fbd-aff8-47e0e72691de", + "name": "update-test-block", + "description": "", + "method": "PUT", + "body": { + "mimeType": "multipart/form-data", + "params": [ + { + "id": "pair_5173b39b1c1e406f8bb56dd4079ba5af", + "name": "block_name", + "value": "First calculator operations test", + "description": "" + }, + { + "id": "pair_ede3d4a98a15489abc3aadb0aa42211d", + "name": "language_uuid", + "value": "19c11b28-3d06-4c54-8946-6af8a28e8b07", + "description": "" + }, + { + "id": "pair_a3ab32838c904b648f6076662ced16ab", + "name": "test_archive", + "value": "", + "description": "", + "type": "file", + "fileName": "/home/pacq/IdeaProjects/java.zip" + } + ] + }, + "parameters": [], + "headers": [ + { "name": "Content-Type", "value": "multipart/form-data" }, + { "name": "User-Agent", "value": "insomnia/8.5.1" } + ], + "authentication": {}, + "metaSortKey": -1703898807668, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "fld_0f115af031f64a478dc8707d5aba1048", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703867530994, + "created": 1703867530994, + "name": "laboratories", + "description": "", + "environment": {}, + "environmentPropertyOrder": null, + "metaSortKey": -1703867530994, + "_type": "request_group" + }, + { + "_id": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "parentId": null, + "modified": 1703867428111, + "created": 1703867428111, + "name": "UPB Codelabs Multipart Form Request", + "description": "", + "scope": "collection", + "_type": "workspace" + }, + { + "_id": "req_1f5b243091ed48d19b56b9e9f1b8b60a", + "parentId": "fld_0f115af031f64a478dc8707d5aba1048", + "modified": 1703877462736, + "created": 1703867533491, + "url": "{{ _.BASE_URL }}/laboratories/test_blocks/77ab37da-580e-44d6-86d3-d3334cb24b12", + "name": "create-test-block", + "description": "", + "method": "POST", + "body": { + "mimeType": "multipart/form-data", + "params": [ + { + "id": "pair_a735916e2a8a49779538d59b17038494", + "name": "language_uuid", + "value": "19c11b28-3d06-4c54-8946-6af8a28e8b07", + "description": "" + }, + { + "id": "pair_08961c834b7f4eba9b58187135414cd6", + "name": "block_name", + "value": "Calculator operations", + "description": "" + }, + { + "id": "pair_a3253709f396429dbbdb03f522270092", + "name": "test_archive", + "value": "", + "description": "", + "type": "file", + "fileName": "/home/pacq/IdeaProjects/java.zip" + } + ] + }, + "parameters": [], + "headers": [ + { "name": "Content-Type", "value": "multipart/form-data" }, + { "name": "User-Agent", "value": "insomnia/8.5.1" } + ], + "authentication": {}, + "metaSortKey": -1703867533491, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "req_81c24080612643dea636b43cba6a5c72", + "parentId": "fld_e99fc9329e1e4e9ea4aad5ac37ff965c", + "modified": 1703899153011, + "created": 1703867446159, + "url": "{{ _.BASE_URL }}/session/login", + "name": "login-as-teacher", + "description": "", + "method": "POST", + "body": { + "mimeType": "application/json", + "text": "{\n \"email\": \"veli.ayele.2020@upb.edu.co\", \n \"password\": \"upbbbga2023*/\"\n}" + }, + "parameters": [], + "headers": [ + { "name": "Content-Type", "value": "application/json" }, + { "name": "User-Agent", "value": "insomnia/8.5.1" } + ], + "authentication": {}, + "metaSortKey": -1703867446159, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "fld_e99fc9329e1e4e9ea4aad5ac37ff965c", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703867444303, + "created": 1703867444303, + "name": "session", + "description": "", + "environment": {}, + "environmentPropertyOrder": null, + "metaSortKey": -1703867444303, + "_type": "request_group" + }, + { + "_id": "env_5128b6531490f5b58316ee3db66aec4f90c368ff", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703867471567, + "created": 1703867428113, + "name": "Base Environment", + "data": { "BASE_URL": "http://127.0.0.1:8080/api/v1" }, + "dataPropertyOrder": { "&": ["BASE_URL"] }, + "color": null, + "isPrivate": false, + "metaSortKey": 1703867428113, + "_type": "environment" + }, + { + "_id": "jar_5128b6531490f5b58316ee3db66aec4f90c368ff", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703899153428, + "created": 1703867428114, + "name": "Default Jar", + "cookies": [ + { + "key": "session", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiNWM2NjY5ZDgtODFiYi00NGEyLTk4YTMtMDRhNTBmY2I1NWU1Iiwicm9sZSI6InRlYWNoZXIiLCJpc3MiOiJjb2RlbGFicyIsImV4cCI6MTcwMzkyMDc1MywibmJmIjoxNzAzODk5MTUzLCJpYXQiOjE3MDM4OTkxNTN9.S9LaOnSyqZXngnBoFzIQdd5fjvSrA4uouPIYpjWs5RA", + "maxAge": 21600, + "domain": "127.0.0.1", + "path": "/", + "httpOnly": true, + "hostOnly": true, + "creation": "2023-12-29T16:32:00.273Z", + "lastAccessed": "2023-12-30T01:19:13.427Z", + "id": "a921c5dd-cc15-4419-88f1-0266c5f22db4" + } + ], + "_type": "cookie_jar" + } + ] +} diff --git a/docs/openapi/spec.openapi.yaml b/docs/openapi/spec.openapi.yaml index 32af95b..8abc92a 100644 --- a/docs/openapi/spec.openapi.yaml +++ b/docs/openapi/spec.openapi.yaml @@ -16,6 +16,8 @@ tags: - name: Laboratories - name: Blocks - name: Rubrics + - name: Languages + - name: Submissions paths: /accounts/admins: @@ -748,6 +750,83 @@ paths: schema: $ref: "#/components/schemas/default_error_response" + # Languages + /languages: + get: + tags: + - Languages + security: + - cookieAuth: [] + description: Get the list of supported languages and its UUIDs. + responses: + "200": + description: The languages are listed + content: + application/json: + schema: + type: object + properties: + languages: + type: array + items: + type: object + allOf: + - $ref: "#/components/schemas/language" + "403": + description: The session token isn't valid or the user doesn't have enough permissions. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "500": + description: There was an unexpected error in the server side. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + + /languages/{language_uuid}/template: + get: + tags: + - Languages + security: + - cookieAuth: [] + parameters: + - in: path + name: language_uuid + schema: + type: string + example: "c3049450-d1cc-424f-a06b-9b3e5c916319" + required: true + description: Get `.zip` archive with the template for the given language + responses: + "200": + description: The `.zip` archive is retreived / downloaded + content: + application/zip: + schema: + type: string + format: binary + "404": + description: No template found with the given UUID. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "403": + description: The session token isn't valid or the user doesn't have enough permissions. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "500": + description: There was an unexpected error in the server side. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + + # Laboratories /laboratories: post: tags: @@ -929,7 +1008,7 @@ paths: required: true requestBody: content: - application/json: + multipart/form-data: schema: $ref: "#/components/schemas/create_test_block_req" responses: @@ -961,9 +1040,107 @@ paths: application/json: schema: $ref: "#/components/schemas/default_error_response" + + /laboratories/{laboratory_uuid}/progress: + get: + tags: + - Laboratories + security: + - cookieAuth: [] + description: Get information about the students' progress in the laboratory. + parameters: + - in: path + name: laboratory_uuid + schema: + type: string + example: "1f071796-c01b-458b-8949-665592d90986" + required: true + responses: + "200": + description: The information about the progress was retreived successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/laboratory_progress_metadata" + "400": + description: Required fields were missed or doesn't fulfill the required format. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "403": + description: The session token isn't valid or the user doesn't have enough permissions. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "404": + description: No laboratory found with the given UUId. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "500": + description: There was an unexpected error in the server side. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + + /laboratories/{laboratory_uuid}/progress/{student_uuid}: + get: + tags: + - Laboratories + security: + - cookieAuth: [] + description: Get information about the progress of an specific student. + parameters: + - in: path + name: laboratory_uuid + schema: + type: string + example: "1f071796-c01b-458b-8949-665592d90986" + required: true + - in: path + name: student_uuid + schema: + type: string + example: "e66d2cff-9a50-47cd-8301-bdf479e6d80e" + required: true + responses: + "200": + description: The information about the progress of the student was retreived successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/student_progress_metadata" + "400": + description: Required fields were missed or doesn't fulfill the required format. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "403": + description: The session token isn't valid or the user doesn't have enough permissions. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "404": + description: No laboratory found with the given UUId. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "500": + description: There was an unexpected error in the server side. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" # Blocks - /markdown_blocks/{block_uuid}: + /blocks/markdown_blocks/{block_uuid}: delete: tags: - Blocks @@ -993,7 +1170,7 @@ paths: schema: $ref: "#/components/schemas/default_error_response" - /markdown_blocks/{block_uuid}/content: + /blocks/markdown_blocks/{block_uuid}/content: patch: tags: - Blocks @@ -1032,7 +1209,7 @@ paths: schema: $ref: "#/components/schemas/default_error_response" - /test_blocks/{block_uuid}: + /blocks/test_blocks/{block_uuid}: put: tags: - Blocks @@ -1048,7 +1225,7 @@ paths: required: true requestBody: content: - application/json: + multipart/form-data: schema: $ref: "#/components/schemas/create_test_block_req" responses: @@ -1516,6 +1693,92 @@ paths: application/json: schema: $ref: "#/components/schemas/default_error_response" + + # Submissions + /submissions/test_block/{block_uuid}/mine: + get: + tags: + - Submissions + security: + - cookieAuth: [] + parameters: + - in: path + name: block_uuid + schema: + type: string + example: "dd5a2edf-8439-4fdc-97e8-6f0d45d6540a" + required: true + description: Get the details / metadata of the student's submission for the given test block + responses: + "200": + description: The laboratory information was retrieved successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/submission_metadata" + "403": + description: The session token isn't valid or the user doesn't have enough permissions. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "404": + description: The student doesn't have any submission for the given test block. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "500": + description: There was an unexpected error in the server side. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + + /submissions/test_block/{block_uuid}: + post: + tags: + - Submissions + security: + - cookieAuth: [] + parameters: + - in: path + name: block_uuid + schema: + type: string + example: "dd5a2edf-8439-4fdc-97e8-6f0d45d6540a" + required: true + description: Submit a `.zip` archive to a test block. + responses: + "201": + description: The `.zip` archive was submitted. + content: + application/json: + schema: + type: object + properties: + uuid: + type: string + example: "325fbfa1-bd9b-4846-8fdd-8383b0e1f857" + "400": + description: Required fields were missed or doesn't fulfill the required format. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "403": + description: The session token isn't valid or the user doesn't have enough permissions. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + "500": + description: There was an unexpected error in the server side. + content: + application/json: + schema: + $ref: "#/components/schemas/default_error_response" + components: securitySchemes: cookieAuth: @@ -1586,16 +1849,15 @@ components: create_test_block_req: type: object properties: - title: + block_name: type: string example: "Test métodos de la lista simplemente enlazada" - language: + language_uuid: type: string - example: "Java" - file: + example: "a7c4c843-c3ef-4083-8255-927aea3af77f" + test_archive: type: string - format: byte - example: "file.java" # T be defined + format: binary # A `.zip` archive create_rubric_req: type: object @@ -1703,7 +1965,7 @@ components: example: "Estructuras de datos NRC 47158" color: type: string - description: "A randomly choosed hexadecimal color" + description: "A randomly chosen hexadecimal color" example: "#34d399" public_laboratory_fields: @@ -1771,6 +2033,16 @@ components: example: 0.25 # Entities + language: + type: object + properties: + uuid: + type: string + example: "c3049450-d1cc-424f-a06b-9b3e5c916319" + name: + type: string + example: "Java" + markdown_block: type: object properties: @@ -1786,6 +2058,7 @@ components: test_block: type: object + required: ["uuid", "language_uuid", "test_archive_uuid", "name", "order"] properties: uuid: type: string @@ -1796,6 +2069,10 @@ components: test_archive_uuid: type: string example: "7f5e5fd3-ad44-4441-8201-cfab68ed1e00" + # (Optional) The UUID of the student' submission + submission_uuid: + type: string + example: "962022d9-8a0a-4686-b10e-fca700601772" name: type: string example: "Duubly Linked List Methods" @@ -1832,4 +2109,72 @@ components: items: type: object allOf: - - $ref: "#/components/schemas/test_block" \ No newline at end of file + - $ref: "#/components/schemas/test_block" + + laboratory_progress_metadata: + type: object + properties: + total_test_blocks: + type: number + example: 8 + progress: + type: array + items: + type: object + properties: + student: + type: string + example: "Chanel Durk" + completed: + type: number + example: 2 + + submission_metadata: + type: object + properties: + uuid: + type: string + example: "2c8d6612-09f5-47d5-b55d-b2f3c26c4ba9" + archive_uuid: + type: string + example: "3ddd8d16-157d-409a-b65a-390f7ac5550c" + passing: + type: boolean + example: true + status: + type: string + enum: ["pending", "running", "ready"] + example: "running" + stdout: + type: string + example: "[ERROR] SinglyLinkedList.addTest(): Expected 1 received 0" + + student_progress_metadata: + type: object + properties: + total_test_blocks: + type: number + example: 8 + passing_submissions: + type: number + example: 4 + submissions: + type: array + items: + $ref: "#/components/schemas/student_submission_metadata" + + student_submission_metadata: + type: object + properties: + uuid: + type: string + example: "2c8d6612-09f5-47d5-b55d-b2f3c26c4ba9" + archive_uuid: + type: string + example: "3ddd8d16-157d-409a-b65a-390f7ac5550c" + test_block_name: + type: string + example: "Doubly Linked List Methods" + passing: + type: boolean + example: True \ No newline at end of file diff --git a/go.mod b/go.mod index b6fe905..d1a46f5 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect - github.com/knz/go-libedit v1.10.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 9bd0a58..acb6545 100644 --- a/go.sum +++ b/go.sum @@ -31,28 +31,20 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= -github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -80,22 +72,15 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -111,15 +96,12 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= @@ -128,7 +110,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -138,8 +119,6 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -149,7 +128,6 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= @@ -171,8 +149,6 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -188,7 +164,6 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -204,17 +179,12 @@ golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/sql/migrations/20230920232901_init.down.sql b/sql/migrations/20230920232901_init.down.sql index cec0e24..dea25a7 100644 --- a/sql/migrations/20230920232901_init.down.sql +++ b/sql/migrations/20230920232901_init.down.sql @@ -29,6 +29,8 @@ DROP VIEW IF EXISTS objectives_owners; DROP VIEW IF EXISTS criteria_owners; -- ## Tables +DROP TABLE IF EXISTS archives; + DROP TABLE IF EXISTS grade_has_criteria; DROP TABLE IF EXISTS grades; diff --git a/sql/migrations/20230920232901_init.up.sql b/sql/migrations/20230920232901_init.up.sql index d68cd03..ba5941c 100644 --- a/sql/migrations/20230920232901_init.up.sql +++ b/sql/migrations/20230920232901_init.up.sql @@ -54,7 +54,7 @@ CREATE TABLE IF NOT EXISTS rubrics ( CREATE TABLE IF NOT EXISTS objectives ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), "rubric_id" UUID NOT NULL REFERENCES rubrics(id), - "description" VARCHAR(510) NOT NULL, + "description" VARCHAR(510) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -69,10 +69,12 @@ CREATE TABLE IF NOT EXISTS criteria ( CREATE TABLE IF NOT EXISTS laboratories ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), "course_id" UUID NOT NULL REFERENCES courses(id), - "rubric_id" UUID DEFAULT NULL REFERENCES rubrics(id) ON DELETE SET DEFAULT, - "name" VARCHAR(255) NOT NULL, - "opening_date" TIMESTAMP NOT NULL, - "due_date" TIMESTAMP NOT NULL + "rubric_id" UUID DEFAULT NULL REFERENCES rubrics(id) ON DELETE + SET + DEFAULT, + "name" VARCHAR(255) NOT NULL, + "opening_date" TIMESTAMP NOT NULL, + "due_date" TIMESTAMP NOT NULL ); CREATE TABLE IF NOT EXISTS blocks_index ( @@ -88,17 +90,22 @@ CREATE TABLE IF NOT EXISTS markdown_blocks ( "content" TEXT NOT NULL DEFAULT '' ); +CREATE TABLE IF NOT EXISTS archives ( + "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "file_id" UUID NOT NULL UNIQUE +); + CREATE TABLE IF NOT EXISTS languages ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "name" VARCHAR(32) NOT NULL UNIQUE, - "base_archive" BYTEA NOT NULL + "template_archive_id" UUID NOT NULL UNIQUE REFERENCES archives(id), + "name" VARCHAR(32) NOT NULL UNIQUE ); CREATE TABLE IF NOT EXISTS test_blocks ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "laboratory_id" UUID NOT NULL REFERENCES laboratories(id), "language_id" UUID NOT NULL REFERENCES languages(id), - "tests_archive_id" BYTEA NOT NULL, + "test_archive_id" UUID NOT NULL UNIQUE REFERENCES archives(id), + "laboratory_id" UUID NOT NULL REFERENCES laboratories(id), "block_index_id" UUID NOT NULL REFERENCES blocks_index(id) ON DELETE CASCADE, "name" VARCHAR(255) NOT NULL ); @@ -107,12 +114,11 @@ CREATE TABLE IF NOT EXISTS submissions ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), "test_id" UUID NOT NULL REFERENCES test_blocks(id), "student_id" UUID NOT NULL REFERENCES users(id), - "archive" BYTEA NOT NULL, + "archive_id" UUID NOT NULL UNIQUE REFERENCES archives(id), "passing" BOOLEAN NOT NULL DEFAULT FALSE, "status" SUBMISSION_STATUS NOT NULL DEFAULT 'pending', "stdout" TEXT NOT NULL DEFAULT '', - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + "submitted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS grades ( @@ -141,12 +147,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_blocks_index ON blocks_index(laboratory_id -- ### Search indexes CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); + CREATE INDEX IF NOT EXISTS idx_users_lower_fullName ON users(LOWER(full_name)); -- ## Views --- ### Users CREATE -OR REPLACE VIEW users_with_creator AS +OR REPLACE VIEW users_with_creator AS SELECT users.id, users.role, @@ -230,10 +237,10 @@ BEGIN END $$ ; -CREATE OR REPLACE TRIGGER set_created_by - BEFORE INSERT ON users - FOR EACH ROW - EXECUTE PROCEDURE update_created_by(); +CREATE +OR REPLACE TRIGGER set_created_by BEFORE +INSERT + ON users FOR EACH ROW EXECUTE PROCEDURE update_created_by(); -- ## Data -- ### Colors @@ -247,6 +254,35 @@ VALUES ('#fbbf24'), ('#f472b6'); +-- ### Languages +DO $$ + +DECLARE + JAVA_FILESYSTEM_ARCHIVE_UUID UUID; + JAVA_DB_ARCHIVE_UUID UUID; + +BEGIN + JAVA_FILESYSTEM_ARCHIVE_UUID := '487034c9-441c-4fb9-b0f3-8f4dd6176532'; + + INSERT INTO + archives (file_id) + VALUES + (JAVA_FILESYSTEM_ARCHIVE_UUID) + RETURNING + id + INTO + JAVA_DB_ARCHIVE_UUID; + + INSERT INTO + languages (name, template_archive_id) + VALUES + ( + 'Java', + JAVA_DB_ARCHIVE_UUID + ); + +END $$; + -- ### Admin user (To be used in development) INSERT INTO users ( diff --git a/src/blocks/application/use_cases.go b/src/blocks/application/use_cases.go index eaa89c3..bce52e1 100644 --- a/src/blocks/application/use_cases.go +++ b/src/blocks/application/use_cases.go @@ -4,10 +4,12 @@ import ( "github.com/UPB-Code-Labs/main-api/src/blocks/domain/definitions" "github.com/UPB-Code-Labs/main-api/src/blocks/domain/dtos" "github.com/UPB-Code-Labs/main-api/src/blocks/domain/errors" + languagesDefinitions "github.com/UPB-Code-Labs/main-api/src/languages/domain/definitions" ) type BlocksUseCases struct { - BlocksRepository definitions.BlockRepository + BlocksRepository definitions.BlockRepository + LanguagesRepository languagesDefinitions.LanguagesRepository } func (useCases *BlocksUseCases) UpdateMarkdownBlockContent(dto dtos.UpdateMarkdownBlockContentDTO) (err error) { @@ -24,3 +26,39 @@ func (useCases *BlocksUseCases) UpdateMarkdownBlockContent(dto dtos.UpdateMarkdo // Update the block return useCases.BlocksRepository.UpdateMarkdownBlockContent(dto.BlockUUID, dto.Content) } + +func (useCases *BlocksUseCases) UpdateTestBlock(dto dtos.UpdateTestBlockDTO) (err error) { + // Validate the teacher is the owner of the block + ownsBlock, err := useCases.BlocksRepository.DoesTeacherOwnsTestBlock(dto.TeacherUUID, dto.BlockUUID) + if err != nil { + return err + } + + if !ownsBlock { + return errors.TeacherDoesNotOwnBlock{} + } + + // Validate the programming language exists + _, err = useCases.LanguagesRepository.GetByUUID(dto.LanguageUUID) + if err != nil { + return err + } + + // Overwrite the block's tests archive if the teacher uploaded a new one + if dto.NewTestArchive != nil { + // Get the UUID of the block's tests archive + uuid, err := useCases.BlocksRepository.GetTestArchiveUUIDFromTestBlockUUID(dto.BlockUUID) + if err != nil { + return err + } + + // Send the request to the microservice + err = useCases.BlocksRepository.OverwriteTestsArchive(uuid, dto.NewTestArchive) + if err != nil { + return err + } + } + + // Update the block + return useCases.BlocksRepository.UpdateTestBlock(&dto) +} diff --git a/src/blocks/domain/definitions/blocks_repository.go b/src/blocks/domain/definitions/blocks_repository.go index 6997ccb..a00da17 100644 --- a/src/blocks/domain/definitions/blocks_repository.go +++ b/src/blocks/domain/definitions/blocks_repository.go @@ -1,6 +1,28 @@ package definitions +import ( + "mime/multipart" + + "github.com/UPB-Code-Labs/main-api/src/blocks/domain/dtos" +) + type BlockRepository interface { + // Update the markdown text of a markdown block UpdateMarkdownBlockContent(blockUUID string, content string) (err error) + + // Functions to check blocks ownership DoesTeacherOwnsMarkdownBlock(teacherUUID string, blockUUID string) (bool, error) + DoesTeacherOwnsTestBlock(teacherUUID string, blockUUID string) (bool, error) + + // Create a new test block + SaveTestsArchive(file *multipart.File) (uuid string, err error) + + // Get the UUID of the `zip` archive saved in the static files microservice + GetTestArchiveUUIDFromTestBlockUUID(blockUUID string) (uuid string, err error) + + // Overwrite the `zip` archive saved in the static files microservice + OverwriteTestsArchive(uuid string, file *multipart.File) (err error) + + // Update the test block information in the database + UpdateTestBlock(*dtos.UpdateTestBlockDTO) (err error) } diff --git a/src/blocks/domain/dtos/blocks_dtos.go b/src/blocks/domain/dtos/blocks_dtos.go index d3b31c4..60fe4c2 100644 --- a/src/blocks/domain/dtos/blocks_dtos.go +++ b/src/blocks/domain/dtos/blocks_dtos.go @@ -1,7 +1,17 @@ package dtos +import "mime/multipart" + type UpdateMarkdownBlockContentDTO struct { TeacherUUID string BlockUUID string Content string } + +type UpdateTestBlockDTO struct { + TeacherUUID string + BlockUUID string + LanguageUUID string + Name string + NewTestArchive *multipart.File +} diff --git a/src/blocks/domain/errors/teacher_does_not_owns_block.go b/src/blocks/domain/errors/blocks_errors.go similarity index 56% rename from src/blocks/domain/errors/teacher_does_not_owns_block.go rename to src/blocks/domain/errors/blocks_errors.go index 535abe8..6dc7855 100644 --- a/src/blocks/domain/errors/teacher_does_not_owns_block.go +++ b/src/blocks/domain/errors/blocks_errors.go @@ -11,3 +11,13 @@ func (err TeacherDoesNotOwnBlock) Error() string { func (err TeacherDoesNotOwnBlock) StatusCode() int { return http.StatusForbidden } + +type BlockNotFound struct{} + +func (err BlockNotFound) Error() string { + return "No block was found with the given UUID" +} + +func (err BlockNotFound) StatusCode() int { + return http.StatusNotFound +} diff --git a/src/blocks/infrastructure/http/controllers.go b/src/blocks/infrastructure/http/controllers.go index ebcee7b..a7a5bcc 100644 --- a/src/blocks/infrastructure/http/controllers.go +++ b/src/blocks/infrastructure/http/controllers.go @@ -6,7 +6,7 @@ import ( "github.com/UPB-Code-Labs/main-api/src/blocks/application" "github.com/UPB-Code-Labs/main-api/src/blocks/domain/dtos" "github.com/UPB-Code-Labs/main-api/src/blocks/infrastructure/requests" - shared_infrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" + sharedInfrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" "github.com/gin-gonic/gin" ) @@ -19,7 +19,7 @@ func (controller *BlocksController) HandleUpdateMarkdownBlockContent(c *gin.Cont blockUUID := c.Param("block_uuid") // Validate the block UUID - if err := shared_infrastructure.GetValidator().Var(blockUUID, "uuid4"); err != nil { + if err := sharedInfrastructure.GetValidator().Var(blockUUID, "uuid4"); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": "Block UUID is not valid", }) @@ -36,7 +36,7 @@ func (controller *BlocksController) HandleUpdateMarkdownBlockContent(c *gin.Cont } // Validate request body - if err := shared_infrastructure.GetValidator().Struct(request); err != nil { + if err := sharedInfrastructure.GetValidator().Struct(request); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": "Validation error", "errors": err.Error(), @@ -58,3 +58,70 @@ func (controller *BlocksController) HandleUpdateMarkdownBlockContent(c *gin.Cont c.Status(http.StatusNoContent) } + +func (controller *BlocksController) HandleUpdateTestBlock(c *gin.Context) { + teacherUUID := c.GetString("session_uuid") + blockUUID := c.Param("block_uuid") + + // Validate the request struct + languageUUID := c.PostForm("language_uuid") + blockName := c.PostForm("block_name") + + req := requests.UpdateTestBlockRequest{ + LanguageUUID: languageUUID, + Name: blockName, + } + + if err := sharedInfrastructure.GetValidator().Struct(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Validation error", + "errors": err.Error(), + }) + return + } + + // Create the DTO + dto := dtos.UpdateTestBlockDTO{ + TeacherUUID: teacherUUID, + BlockUUID: blockUUID, + LanguageUUID: languageUUID, + Name: blockName, + } + + // Validate the test archive (if any) + multipartHeader, err := c.FormFile("test_archive") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Please, make sure to send the test archive", + }) + return + } + + if multipartHeader != nil { + err = sharedInfrastructure.ValidateMultipartFileHeader(multipartHeader) + if err != nil { + c.Error(err) + return + } + + // Add the test archive to the DTO + multipartFile, err := multipartHeader.Open() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "There was an error while reading the test archive", + }) + return + } + + dto.NewTestArchive = &multipartFile + } + + // Update the test block + err = controller.UseCases.UpdateTestBlock(dto) + if err != nil { + c.Error(err) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/src/blocks/infrastructure/http/routes.go b/src/blocks/infrastructure/http/routes.go index 54b1a43..3987040 100644 --- a/src/blocks/infrastructure/http/routes.go +++ b/src/blocks/infrastructure/http/routes.go @@ -3,7 +3,8 @@ package http import ( "github.com/UPB-Code-Labs/main-api/src/blocks/application" "github.com/UPB-Code-Labs/main-api/src/blocks/infrastructure/implementations" - shared_infrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" + languagesImplementations "github.com/UPB-Code-Labs/main-api/src/languages/infrastructure/implementations" + sharedInfrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" "github.com/gin-gonic/gin" ) @@ -11,17 +12,25 @@ func StartBlocksRoutes(g *gin.RouterGroup) { blocksGroup := g.Group("/blocks") useCases := application.BlocksUseCases{ - BlocksRepository: implementations.GetBlocksPostgresRepositoryInstance(), + BlocksRepository: implementations.GetBlocksPostgresRepositoryInstance(), + LanguagesRepository: languagesImplementations.GetLanguagesRepositoryInstance(), } controller := BlocksController{ UseCases: &useCases, } - blocksGroup.PUT( + blocksGroup.PATCH( "/markdown_blocks/:block_uuid/content", - shared_infrastructure.WithAuthenticationMiddleware(), - shared_infrastructure.WithAuthorizationMiddleware([]string{"teacher"}), + sharedInfrastructure.WithAuthenticationMiddleware(), + sharedInfrastructure.WithAuthorizationMiddleware([]string{"teacher"}), controller.HandleUpdateMarkdownBlockContent, ) + + blocksGroup.PUT( + "/test_blocks/:block_uuid", + sharedInfrastructure.WithAuthenticationMiddleware(), + sharedInfrastructure.WithAuthorizationMiddleware([]string{"teacher"}), + controller.HandleUpdateTestBlock, + ) } diff --git a/src/blocks/infrastructure/implementations/blocks_repository.go b/src/blocks/infrastructure/implementations/blocks_repository.go index 16ca798..20aafe7 100644 --- a/src/blocks/infrastructure/implementations/blocks_repository.go +++ b/src/blocks/infrastructure/implementations/blocks_repository.go @@ -1,11 +1,21 @@ package implementations import ( + "bytes" "context" "database/sql" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" "time" - "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" + "github.com/UPB-Code-Labs/main-api/src/blocks/domain/dtos" + "github.com/UPB-Code-Labs/main-api/src/blocks/domain/errors" + sharedDomainErrors "github.com/UPB-Code-Labs/main-api/src/shared/domain/errors" + sharedInfrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" ) type BlocksPostgresRepository struct { @@ -18,7 +28,7 @@ var blocksPostgresRepositoryInstance *BlocksPostgresRepository func GetBlocksPostgresRepositoryInstance() *BlocksPostgresRepository { if blocksPostgresRepositoryInstance == nil { blocksPostgresRepositoryInstance = &BlocksPostgresRepository{ - Connection: infrastructure.GetPostgresConnection(), + Connection: sharedInfrastructure.GetPostgresConnection(), } } @@ -79,3 +89,248 @@ func (repository *BlocksPostgresRepository) DoesTeacherOwnsMarkdownBlock(teacher return laboratoryTeacherUUID == teacherUUID, nil } + +func (repository *BlocksPostgresRepository) DoesTeacherOwnsTestBlock(teacherUUID string, blockUUID string) (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + // Get the UUID of the laboratory the block belongs to + query := ` + SELECT laboratory_id + FROM test_blocks + WHERE id = $1 + ` + + row := repository.Connection.QueryRowContext(ctx, query, blockUUID) + var laboratoryUUID string + if err := row.Scan(&laboratoryUUID); err != nil { + if err == sql.ErrNoRows { + return false, nil + } + } + + // Check if the teacher owns the laboratory + query = ` + SELECT teacher_id + FROM courses + WHERE id = ( + SELECT course_id + FROM laboratories + WHERE id = $1 + ) + ` + + row = repository.Connection.QueryRowContext(ctx, query, laboratoryUUID) + var laboratoryTeacherUUID string + if err := row.Scan(&laboratoryTeacherUUID); err != nil { + if err == sql.ErrNoRows { + return false, nil + } + } + + return laboratoryTeacherUUID == teacherUUID, nil +} + +func (repository *BlocksPostgresRepository) GetTestArchiveUUIDFromTestBlockUUID(blockUUID string) (uuid string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + query := ` + SELECT file_id + FROM archives + WHERE id = ( + SELECT test_archive_id + FROM test_blocks + WHERE id = $1 + ) + ` + + row := repository.Connection.QueryRowContext(ctx, query, blockUUID) + + // Parse the row + err = row.Scan(&uuid) + if err != nil { + if err == sql.ErrNoRows { + return "", &errors.BlockNotFound{} + } + + return "", err + } + + return uuid, nil +} + +func (repository *BlocksPostgresRepository) SaveTestsArchive(file *multipart.File) (uuid string, err error) { + // Create multipart writer + staticFilesEndpoint := fmt.Sprintf("%s/archives/save", sharedInfrastructure.GetEnvironment().StaticFilesMicroserviceAddress) + baseMultipartBuffer, err := repository.getMultipartFormBuffer( + staticFilesEndpoint, + file, + ) + if err != nil { + return "", err + } + + // Add the file type field to the request + err = baseMultipartBuffer.bodyBufferWriter.WriteField("archive_type", "test") + if err != nil { + return "", err + } + + // Close the writer + err = baseMultipartBuffer.bodyBufferWriter.Close() + if err != nil { + return "", err + } + + // Prepare the request + req, err := http.NewRequest("POST", staticFilesEndpoint, baseMultipartBuffer.bodyBuffer) + + if err != nil { + return "", err + } + + req.Header.Set("Content-Type", baseMultipartBuffer.bodyBufferWriter.FormDataContentType()) + + // Send the request + client := &http.Client{} + res, err := client.Do(req) + + // Forward error message if any + microserviceError := sharedInfrastructure.ParseMicroserviceError(res, err) + if microserviceError != nil { + return "", microserviceError + } + + // Return the UUID of the saved file + var response map[string]interface{} + err = json.NewDecoder(res.Body).Decode(&response) + if err != nil { + return "", err + } + + if response["uuid"] == nil { + return "", &sharedDomainErrors.GenericDomainError{ + Code: http.StatusInternalServerError, + Message: "The static files microservice did not return the UUID of the saved file", + } + } + + return response["uuid"].(string), nil +} + +func (repository *BlocksPostgresRepository) OverwriteTestsArchive(uuid string, file *multipart.File) (err error) { + // Create multipart writer + staticFilesEndpoint := fmt.Sprintf("%s/archives/overwrite", sharedInfrastructure.GetEnvironment().StaticFilesMicroserviceAddress) + baseMultipartBuffer, err := repository.getMultipartFormBuffer( + staticFilesEndpoint, + file, + ) + if err != nil { + return err + } + + // Add the file type field to the request + err = baseMultipartBuffer.bodyBufferWriter.WriteField("archive_type", "test") + if err != nil { + return err + } + + // Add the archive uuid field to the request + err = baseMultipartBuffer.bodyBufferWriter.WriteField("archive_uuid", uuid) + if err != nil { + return err + } + + // Close the writer + err = baseMultipartBuffer.bodyBufferWriter.Close() + if err != nil { + return err + } + + // Prepare the request + req, err := http.NewRequest("PUT", staticFilesEndpoint, baseMultipartBuffer.bodyBuffer) + + if err != nil { + return err + } + + req.Header.Set("Content-Type", baseMultipartBuffer.bodyBufferWriter.FormDataContentType()) + + // Send the request + client := &http.Client{} + res, err := client.Do(req) + + // Forward error message if any + microserviceError := sharedInfrastructure.ParseMicroserviceError(res, err) + if microserviceError != nil { + return microserviceError + } + + return nil +} + +type baseMultipartFormBuffer struct { + bodyBuffer *bytes.Buffer + bodyBufferWriter *multipart.Writer +} + +func (repository *BlocksPostgresRepository) getMultipartFormBuffer(endpoint string, file *multipart.File) (br *baseMultipartFormBuffer, err error) { + FILE_NAME := "archive.zip" + FILE_CONTENT_TYPE := "application/zip" + + // Create multipart writer + var bodyBuffer bytes.Buffer + multipartWriter := multipart.NewWriter(&bodyBuffer) + + // Add the file field to the request + header := textproto.MIMEHeader{} + header.Set("Content-Disposition", + fmt.Sprintf( + `form-data; name="%s"; filename="%s"`, + "file", + FILE_NAME, + ), + ) + header.Set("Content-Type", FILE_CONTENT_TYPE) + + // Reset the file pointer to the beginning + _, err = (*file).Seek(0, io.SeekStart) + if err != nil { + return nil, err + } + + // Add the file to the request + fileWriter, err := multipartWriter.CreatePart(header) + if err != nil { + return nil, err + } + + if _, err := io.Copy(fileWriter, *file); err != nil { + return nil, err + } + + return &baseMultipartFormBuffer{ + bodyBuffer: &bodyBuffer, + bodyBufferWriter: multipartWriter, + }, nil +} + +func (repository *BlocksPostgresRepository) UpdateTestBlock(dto *dtos.UpdateTestBlockDTO) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + // Update the block + query := ` + UPDATE test_blocks + SET language_id = $1, name = $2 + WHERE id = $3 + ` + + _, err = repository.Connection.ExecContext(ctx, query, dto.LanguageUUID, dto.Name, dto.BlockUUID) + if err != nil { + return err + } + + return nil +} diff --git a/src/blocks/infrastructure/requests/blocks_requests.go b/src/blocks/infrastructure/requests/blocks_requests.go index 64817fc..c472551 100644 --- a/src/blocks/infrastructure/requests/blocks_requests.go +++ b/src/blocks/infrastructure/requests/blocks_requests.go @@ -3,3 +3,8 @@ package requests type UpdateMarkdownBlockContentRequest struct { Content string `json:"content" validate:"required"` } + +type UpdateTestBlockRequest struct { + LanguageUUID string `validate:"required,uuid4"` + Name string `validate:"required,min=4,max=255"` +} diff --git a/src/config/infrastructure/http_server.go b/src/config/infrastructure/http_server.go index de5840d..0aced4e 100644 --- a/src/config/infrastructure/http_server.go +++ b/src/config/infrastructure/http_server.go @@ -5,6 +5,7 @@ import ( blocks_http "github.com/UPB-Code-Labs/main-api/src/blocks/infrastructure/http" courses_http "github.com/UPB-Code-Labs/main-api/src/courses/infrastructure/http" laboratories_http "github.com/UPB-Code-Labs/main-api/src/laboratories/infrastructure/http" + languages_http "github.com/UPB-Code-Labs/main-api/src/languages/infrastructure/http" rubrics_http "github.com/UPB-Code-Labs/main-api/src/rubrics/infrastructure/http" session_http "github.com/UPB-Code-Labs/main-api/src/session/infrastructure/http" shared_infra "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" @@ -19,6 +20,7 @@ var routesGroups = []func(*gin.RouterGroup){ courses_http.StartCoursesRoutes, rubrics_http.StartRubricsRoutes, laboratories_http.StartLaboratoriesRoutes, + languages_http.StartLanguagesRoutes, } func InstanceHttpServer() (r *gin.Engine) { diff --git a/src/laboratories/application/use_cases.go b/src/laboratories/application/use_cases.go index 5c35ad1..05743b3 100644 --- a/src/laboratories/application/use_cases.go +++ b/src/laboratories/application/use_cases.go @@ -1,19 +1,23 @@ package application import ( + blocksDefinitions "github.com/UPB-Code-Labs/main-api/src/blocks/domain/definitions" courses_definitions "github.com/UPB-Code-Labs/main-api/src/courses/domain/definitions" courses_errors "github.com/UPB-Code-Labs/main-api/src/courses/domain/errors" "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/definitions" "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/dtos" "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/entities" - rubrics_definitions "github.com/UPB-Code-Labs/main-api/src/rubrics/domain/definitions" + languagesDefinitions "github.com/UPB-Code-Labs/main-api/src/languages/domain/definitions" + rubricsDefinitions "github.com/UPB-Code-Labs/main-api/src/rubrics/domain/definitions" rubrics_errors "github.com/UPB-Code-Labs/main-api/src/rubrics/domain/errors" ) type LaboratoriesUseCases struct { - LaboratoriesRepository definitions.LaboratoriesRepository CoursesRepository courses_definitions.CoursesRepository - RubricsRepository rubrics_definitions.RubricsRepository + LaboratoriesRepository definitions.LaboratoriesRepository + RubricsRepository rubricsDefinitions.RubricsRepository + LanguagesRepository languagesDefinitions.LanguagesRepository + BlocksRepository blocksDefinitions.BlockRepository } func (useCases *LaboratoriesUseCases) CreateLaboratory(dto *dtos.CreateLaboratoryDTO) (laboratory *entities.Laboratory, err error) { @@ -100,3 +104,31 @@ func (useCases *LaboratoriesUseCases) doesTeacherOwnsLaboratory(teacherUUID, lab return useCases.CoursesRepository.DoesTeacherOwnsCourse(teacherUUID, laboratory.CourseUUID) } + +func (useCases *LaboratoriesUseCases) CreateTestBlock(dto *dtos.CreateTestBlockDTO) (blockUUID string, err error) { + // Check that the teacher owns the laboratory + teacherOwnsLaboratory, err := useCases.doesTeacherOwnsLaboratory(dto.TeacherUUID, dto.LaboratoryUUID) + if err != nil { + return "", err + } + + if !teacherOwnsLaboratory { + return "", &courses_errors.TeacherDoesNotOwnsCourseError{} + } + + // Check that the language exists + _, err = useCases.LanguagesRepository.GetByUUID(dto.LanguageUUID) + if err != nil { + return "", err + } + + // Send the file to the static files microservice + savedArchiveUUID, err := useCases.BlocksRepository.SaveTestsArchive(dto.MultipartFile) + if err != nil { + return "", err + } + dto.TestArchiveUUID = savedArchiveUUID + + // Save the information in the database + return useCases.LaboratoriesRepository.CreateTestBlock(dto) +} diff --git a/src/laboratories/domain/definitions/laboratories_repository.go b/src/laboratories/domain/definitions/laboratories_repository.go index bbb23d7..c1581db 100644 --- a/src/laboratories/domain/definitions/laboratories_repository.go +++ b/src/laboratories/domain/definitions/laboratories_repository.go @@ -11,4 +11,5 @@ type LaboratoriesRepository interface { UpdateLaboratory(dto *dtos.UpdateLaboratoryDTO) error CreateMarkdownBlock(laboratoryUUID string) (blockUUID string, err error) + CreateTestBlock(dto *dtos.CreateTestBlockDTO) (blockUUID string, err error) } diff --git a/src/laboratories/domain/dtos/laboratories_dtos.go b/src/laboratories/domain/dtos/laboratories_dtos.go index cfc4f1f..e415854 100644 --- a/src/laboratories/domain/dtos/laboratories_dtos.go +++ b/src/laboratories/domain/dtos/laboratories_dtos.go @@ -1,5 +1,7 @@ package dtos +import "mime/multipart" + type CreateLaboratoryDTO struct { TeacherUUID string CourseUUID string @@ -26,3 +28,12 @@ type CreateMarkdownBlockDTO struct { TeacherUUID string LaboratoryUUID string } + +type CreateTestBlockDTO struct { + TeacherUUID string + LaboratoryUUID string + LanguageUUID string + TestArchiveUUID string + Name string + MultipartFile *multipart.File +} diff --git a/src/laboratories/infrastructure/http/controllers.go b/src/laboratories/infrastructure/http/controllers.go index f620e23..6bfb39e 100644 --- a/src/laboratories/infrastructure/http/controllers.go +++ b/src/laboratories/infrastructure/http/controllers.go @@ -180,3 +180,69 @@ func (controller *LaboratoriesController) HandleCreateMarkdownBlock(c *gin.Conte "uuid": blockUUID, }) } + +func (controller *LaboratoriesController) HandleCreateTestBlock(c *gin.Context) { + teacherUUID := c.GetString("session_uuid") + laboratoryUUID := c.Param("laboratory_uuid") + + // Validate the request struct + languageUUID := c.PostForm("language_uuid") + name := c.PostForm("block_name") + + req := requests.CreateTestBlockRequest{ + LaboratoryUUID: laboratoryUUID, + LanguageUUID: languageUUID, + Name: name, + } + + if err := infrastructure.GetValidator().Struct(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Validation error", + "errors": err.Error(), + }) + return + } + + // Validate the test archive + multipartHeader, err := c.FormFile("test_archive") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Please, make sure to send the test archive", + }) + return + } + + err = infrastructure.ValidateMultipartFileHeader(multipartHeader) + if err != nil { + c.Error(err) + return + } + + // Create the DTO + multipartFile, err := multipartHeader.Open() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "There was an error while reading the test archive", + }) + return + } + + dto := dtos.CreateTestBlockDTO{ + LaboratoryUUID: laboratoryUUID, + TeacherUUID: teacherUUID, + LanguageUUID: languageUUID, + Name: name, + MultipartFile: &multipartFile, + } + + // Create the block + blockUUID, err := controller.UseCases.CreateTestBlock(&dto) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "uuid": blockUUID, + }) +} diff --git a/src/laboratories/infrastructure/http/routes.go b/src/laboratories/infrastructure/http/routes.go index c1e2239..c757b3c 100644 --- a/src/laboratories/infrastructure/http/routes.go +++ b/src/laboratories/infrastructure/http/routes.go @@ -1,10 +1,12 @@ package http import ( - courses_implementation "github.com/UPB-Code-Labs/main-api/src/courses/infrastructure/implementations" + blocksImplementation "github.com/UPB-Code-Labs/main-api/src/blocks/infrastructure/implementations" + coursesImplementation "github.com/UPB-Code-Labs/main-api/src/courses/infrastructure/implementations" "github.com/UPB-Code-Labs/main-api/src/laboratories/application" "github.com/UPB-Code-Labs/main-api/src/laboratories/infrastructure/implementations" - rubrics_implementation "github.com/UPB-Code-Labs/main-api/src/rubrics/infrastructure/implementations" + languagesImplementation "github.com/UPB-Code-Labs/main-api/src/languages/infrastructure/implementations" + rubricImplementation "github.com/UPB-Code-Labs/main-api/src/rubrics/infrastructure/implementations" "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" "github.com/gin-gonic/gin" ) @@ -14,8 +16,10 @@ func StartLaboratoriesRoutes(g *gin.RouterGroup) { useCases := application.LaboratoriesUseCases{ LaboratoriesRepository: implementations.GetLaboratoriesPostgresRepositoryInstance(), - CoursesRepository: courses_implementation.GetCoursesPgRepository(), - RubricsRepository: rubrics_implementation.GetRubricsPgRepository(), + CoursesRepository: coursesImplementation.GetCoursesPgRepository(), + RubricsRepository: rubricImplementation.GetRubricsPgRepository(), + LanguagesRepository: languagesImplementation.GetLanguagesRepositoryInstance(), + BlocksRepository: blocksImplementation.GetBlocksPostgresRepositoryInstance(), } controller := LaboratoriesController{ @@ -46,4 +50,10 @@ func StartLaboratoriesRoutes(g *gin.RouterGroup) { infrastructure.WithAuthorizationMiddleware([]string{"teacher"}), controller.HandleCreateMarkdownBlock, ) + laboratoriesGroup.POST( + "/test_blocks/:laboratory_uuid", + infrastructure.WithAuthenticationMiddleware(), + infrastructure.WithAuthorizationMiddleware([]string{"teacher"}), + controller.HandleCreateTestBlock, + ) } diff --git a/src/laboratories/infrastructure/implementations/laboratories_respository.go b/src/laboratories/infrastructure/implementations/laboratories_respository.go index 0a092f7..8546d54 100644 --- a/src/laboratories/infrastructure/implementations/laboratories_respository.go +++ b/src/laboratories/infrastructure/implementations/laboratories_respository.go @@ -110,7 +110,7 @@ func (repository *LaboratoriesPostgresRepository) getTestBlocks(laboratoryUUID s defer cancel() query := ` - SELECT tb.id, tb.language_id, tb.tests_archive_id, tb.name, bi.block_position + SELECT tb.id, tb.language_id, tb.test_archive_id, tb.name, bi.block_position FROM test_blocks tb RIGHT JOIN blocks_index bi ON tb.block_index_id = bi.id WHERE tb.laboratory_id = $1 @@ -215,3 +215,73 @@ func (repository *LaboratoriesPostgresRepository) CreateMarkdownBlock(laboratory // Return the new block UUID return blockUUID, nil } + +func (repository *LaboratoriesPostgresRepository) CreateTestBlock(dto *dtos.CreateTestBlockDTO) (blockUUID string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + // Start transaction + tx, err := repository.Connection.BeginTx(ctx, nil) + if err != nil { + return "", err + } + defer tx.Rollback() + + // Create block index + query := ` + INSERT INTO blocks_index (laboratory_id, block_position) + VALUES ( + $1, + ( SELECT COALESCE(MAX(block_position), 0) + 1 FROM blocks_index WHERE laboratory_id = $1 ) + ) + RETURNING id + ` + + row := tx.QueryRowContext(ctx, query, dto.LaboratoryUUID) + var dbBlockIndexUUID string + if err := row.Scan(&dbBlockIndexUUID); err != nil { + return "", err + } + + // Save the archive metadata + query = ` + INSERT INTO archives (file_id) + VALUES ($1) + RETURNING id + ` + + row = tx.QueryRowContext(ctx, query, dto.TestArchiveUUID) + var dbArchiveUUID string + if err := row.Scan(&dbArchiveUUID); err != nil { + return "", err + } + + // Create test block + query = ` + INSERT INTO test_blocks (language_id, test_archive_id, laboratory_id, block_index_id, name) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + ` + + row = tx.QueryRowContext( + ctx, + query, + dto.LanguageUUID, + dbArchiveUUID, + dto.LaboratoryUUID, + dbBlockIndexUUID, + dto.Name, + ) + + if err := row.Scan(&blockUUID); err != nil { + return "", err + } + + // Commit transaction + if err := tx.Commit(); err != nil { + return "", err + } + + // Return the new block UUID + return blockUUID, nil +} diff --git a/src/laboratories/infrastructure/requests/laboratories_requests.go b/src/laboratories/infrastructure/requests/laboratories_requests.go index 8fc2855..20470be 100644 --- a/src/laboratories/infrastructure/requests/laboratories_requests.go +++ b/src/laboratories/infrastructure/requests/laboratories_requests.go @@ -36,3 +36,9 @@ func (request *UpdateLaboratoryRequest) ToDTO(laboratoryUUID string, teacherUUID DueDate: request.DueDate, } } + +type CreateTestBlockRequest struct { + LaboratoryUUID string `validate:"required,uuid4"` + LanguageUUID string `validate:"required,uuid4"` + Name string `validate:"required,min=4,max=255"` +} diff --git a/src/languages/application/usec_cases.go b/src/languages/application/usec_cases.go new file mode 100644 index 0000000..be04ce3 --- /dev/null +++ b/src/languages/application/usec_cases.go @@ -0,0 +1,25 @@ +package application + +import ( + "github.com/UPB-Code-Labs/main-api/src/languages/domain/definitions" + "github.com/UPB-Code-Labs/main-api/src/languages/domain/entities" +) + +type LanguageUseCases struct { + LanguageRepository definitions.LanguagesRepository +} + +func (useCases *LanguageUseCases) GetLanguages() ([]*entities.Language, error) { + return useCases.LanguageRepository.GetAll() +} + +func (useCases *LanguageUseCases) GetLanguageTemplate(uuid string) ([]byte, error) { + // Get the information of the language from the database + langTemplateUUID, err := useCases.LanguageRepository.GetTemplateArchiveUUIDByLanguageUUID(uuid) + if err != nil { + return nil, err + } + + // Return an empty template bytes array + return useCases.LanguageRepository.GetTemplateBytes(langTemplateUUID) +} diff --git a/src/languages/domain/definitions/languages_repository.go b/src/languages/domain/definitions/languages_repository.go new file mode 100644 index 0000000..a02eaa6 --- /dev/null +++ b/src/languages/domain/definitions/languages_repository.go @@ -0,0 +1,11 @@ +package definitions + +import "github.com/UPB-Code-Labs/main-api/src/languages/domain/entities" + +type LanguagesRepository interface { + GetAll() (languages []*entities.Language, err error) + GetByUUID(uuid string) (language *entities.Language, err error) + + GetTemplateArchiveUUIDByLanguageUUID(uuid string) (templateUUID string, err error) + GetTemplateBytes(uuid string) (template []byte, err error) +} diff --git a/src/languages/domain/dtos/.gitkeep b/src/languages/domain/dtos/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/languages/domain/entities/language.go b/src/languages/domain/entities/language.go new file mode 100644 index 0000000..2fa8049 --- /dev/null +++ b/src/languages/domain/entities/language.go @@ -0,0 +1,7 @@ +package entities + +type Language struct { + UUID string `json:"uuid"` + TemplateArchiveUUID string `json:"-"` + Name string `json:"name"` +} diff --git a/src/languages/domain/errors/languages_errors.go b/src/languages/domain/errors/languages_errors.go new file mode 100644 index 0000000..7b97ecd --- /dev/null +++ b/src/languages/domain/errors/languages_errors.go @@ -0,0 +1,13 @@ +package errors + +import "net/http" + +type LangNotFoundError struct{} + +func (err *LangNotFoundError) Error() string { + return "Language not found" +} + +func (err *LangNotFoundError) StatusCode() int { + return http.StatusNotFound +} diff --git a/src/languages/infrastructure/http/http_controllers.go b/src/languages/infrastructure/http/http_controllers.go new file mode 100644 index 0000000..aca191b --- /dev/null +++ b/src/languages/infrastructure/http/http_controllers.go @@ -0,0 +1,48 @@ +package http + +import ( + "net/http" + + "github.com/UPB-Code-Labs/main-api/src/languages/application" + "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" + "github.com/gin-gonic/gin" +) + +type LanguagesController struct { + UseCases *application.LanguageUseCases +} + +func (controller *LanguagesController) HandleGetLanguages(c *gin.Context) { + languages, err := controller.UseCases.GetLanguages() + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "languages": languages, + }) +} + +func (controller *LanguagesController) HandleDownloadLanguageTemplate(c *gin.Context) { + // Get the language UUID + languageUUID := c.Param("language_uuid") + + // Validate the language UUID + if err := infrastructure.GetValidator().Var(languageUUID, "uuid4"); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Language UUID is not valid", + }) + return + } + + // Get the language template + template, err := controller.UseCases.GetLanguageTemplate(languageUUID) + if err != nil { + c.Error(err) + return + } + + // Return the template + c.Data(http.StatusOK, "application/zip", template) +} diff --git a/src/languages/infrastructure/http/http_routes.go b/src/languages/infrastructure/http/http_routes.go new file mode 100644 index 0000000..49136c0 --- /dev/null +++ b/src/languages/infrastructure/http/http_routes.go @@ -0,0 +1,33 @@ +package http + +import ( + "github.com/UPB-Code-Labs/main-api/src/languages/application" + "github.com/UPB-Code-Labs/main-api/src/languages/infrastructure/implementations" + shared_infrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" + "github.com/gin-gonic/gin" +) + +func StartLanguagesRoutes(g *gin.RouterGroup) { + langGroup := g.Group("/languages") + + useCases := application.LanguageUseCases{ + LanguageRepository: implementations.GetLanguagesRepositoryInstance(), + } + + controllers := LanguagesController{ + UseCases: &useCases, + } + + langGroup.GET( + "", + shared_infrastructure.WithAuthenticationMiddleware(), + shared_infrastructure.WithAuthorizationMiddleware([]string{"teacher", "student"}), + controllers.HandleGetLanguages, + ) + langGroup.GET( + "/:language_uuid/template", + shared_infrastructure.WithAuthenticationMiddleware(), + shared_infrastructure.WithAuthorizationMiddleware([]string{"teacher", "student"}), + controllers.HandleDownloadLanguageTemplate, + ) +} diff --git a/src/languages/infrastructure/implementations/languages_repository.go b/src/languages/infrastructure/implementations/languages_repository.go new file mode 100644 index 0000000..031381b --- /dev/null +++ b/src/languages/infrastructure/implementations/languages_repository.go @@ -0,0 +1,140 @@ +package implementations + +import ( + "context" + "database/sql" + "fmt" + "io" + "net/http" + "time" + + "github.com/UPB-Code-Labs/main-api/src/languages/domain/entities" + "github.com/UPB-Code-Labs/main-api/src/languages/domain/errors" + shared_infrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" +) + +type LanguagesRepository struct { + Connection *sql.DB +} + +// Singleton +var langRepositoryInstance *LanguagesRepository + +func GetLanguagesRepositoryInstance() *LanguagesRepository { + if langRepositoryInstance == nil { + langRepositoryInstance = &LanguagesRepository{ + Connection: shared_infrastructure.GetPostgresConnection(), + } + } + + return langRepositoryInstance +} + +// Methods implementation +func (repository *LanguagesRepository) GetAll() (languages []*entities.Language, err error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + query := ` + SELECT + id, template_archive_id, name + FROM languages + ` + + rows, err := repository.Connection.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + // Parse the rows + for rows.Next() { + var language entities.Language + err := rows.Scan(&language.UUID, &language.TemplateArchiveUUID, &language.Name) + if err != nil { + return nil, err + } + + languages = append(languages, &language) + } + + return languages, nil +} + +func (repository *LanguagesRepository) GetByUUID(uuid string) (language *entities.Language, err error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + query := ` + SELECT + id, template_archive_id, name + FROM languages + WHERE id = $1 + ` + + row := repository.Connection.QueryRowContext(ctx, query, uuid) + + // Parse the row + language = &entities.Language{} + err = row.Scan(&language.UUID, &language.TemplateArchiveUUID, &language.Name) + if err != nil { + if err == sql.ErrNoRows { + return nil, &errors.LangNotFoundError{} + } + + return nil, err + } + + return language, nil +} + +func (repository *LanguagesRepository) GetTemplateArchiveUUIDByLanguageUUID(uuid string) (templateUUID string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + query := ` + SELECT file_id + FROM archives + WHERE id = ( + SELECT + template_archive_id + FROM languages + WHERE id = $1 + ) + ` + + row := repository.Connection.QueryRowContext(ctx, query, uuid) + + // Parse the row + err = row.Scan(&templateUUID) + if err != nil { + if err == sql.ErrNoRows { + return "", &errors.LangNotFoundError{} + } + + return "", err + } + + return templateUUID, nil +} + +func (repository *LanguagesRepository) GetTemplateBytes(uuid string) (template []byte, err error) { + // Send a request to the static files microservice + staticFilesMsEndpoint := fmt.Sprintf("%s/templates/%s", shared_infrastructure.GetEnvironment().StaticFilesMicroserviceAddress, uuid) + resp, err := http.Get(staticFilesMsEndpoint) + + // If there is an error try to forward the error message + microserviceError := shared_infrastructure.ParseMicroserviceError(resp, err) + if microserviceError != nil { + return nil, microserviceError + } + + // Read the body + defer resp.Body.Close() + template, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return template, nil +} diff --git a/src/languages/infrastructure/requests/.gitkeep b/src/languages/infrastructure/requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/domain/errors/errors.go b/src/shared/domain/errors/errors.go index 09851c6..123b14f 100644 --- a/src/shared/domain/errors/errors.go +++ b/src/shared/domain/errors/errors.go @@ -4,3 +4,16 @@ type DomainError interface { Error() string StatusCode() int } + +type GenericDomainError struct { + Code int + Message string +} + +func (err *GenericDomainError) Error() string { + return err.Message +} + +func (err *GenericDomainError) StatusCode() int { + return err.Code +} diff --git a/src/shared/infrastructure/environment.go b/src/shared/infrastructure/environment.go index 39966f7..8347345 100644 --- a/src/shared/infrastructure/environment.go +++ b/src/shared/infrastructure/environment.go @@ -7,12 +7,23 @@ import ( ) type EnvironmentSpec struct { - Environment string `split_words:"true" default:"development"` - DbConnectionString string `split_words:"true" default:"postgres://postgres:postgres@localhost:5432/codelabs?sslmode=disable"` - DbMigrationsPath string `split_words:"true" default:"file://sql/migrations"` + // Execution environment + Environment string `split_words:"true" default:"development"` + + // Connection strings + DbConnectionString string `split_words:"true" default:"postgres://postgres:postgres@localhost:5432/codelabs?sslmode=disable"` + WebClientUrl string `split_words:"true" default:"http://localhost:5173"` + StaticFilesMicroserviceAddress string `split_words:"true" default:"http://localhost:8081"` + + // PgSQL migration files + DbMigrationsPath string `split_words:"true" default:"file://sql/migrations"` + + // JWT parameters JwtSecret string `split_words:"true" default:"default"` JwtExpirationHours int `split_words:"true" default:"6"` - WebClientUrl string `split_words:"true" default:"http://localhost:5173"` + + // Configuration parameters + ArchiveMaxSizeKb int64 `split_words:"true" default:"1024"` } var environment *EnvironmentSpec diff --git a/src/shared/infrastructure/utils.go b/src/shared/infrastructure/utils.go index 6eb826a..565902c 100644 --- a/src/shared/infrastructure/utils.go +++ b/src/shared/infrastructure/utils.go @@ -1,8 +1,101 @@ package infrastructure -import "time" +import ( + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "time" + sharedDomainErrors "github.com/UPB-Code-Labs/main-api/src/shared/domain/errors" + "github.com/gabriel-vasile/mimetype" +) + +// ParseISODate parses a date in ISO format received from a date-time input func ParseISODate(date string) (time.Time, error) { layout := "2006-01-02T15:04" return time.Parse(layout, date) } + +// ValidateMultipartFileHeader validates the multipart archive according to the +// environment configuration and domain rules +func ValidateMultipartFileHeader(multipartHeader *multipart.FileHeader) error { + if multipartHeader.Size > GetEnvironment().ArchiveMaxSizeKb*1024 { + return &sharedDomainErrors.GenericDomainError{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("The archive must be less than %d KB", GetEnvironment().ArchiveMaxSizeKb), + } + } + + file, err := multipartHeader.Open() + if err != nil { + return &sharedDomainErrors.GenericDomainError{ + Code: http.StatusInternalServerError, + Message: "There was an error while reading the test archive", + } + } + defer file.Close() + + mtype, err := mimetype.DetectReader(file) + if err != nil { + return &sharedDomainErrors.GenericDomainError{ + Code: http.StatusInternalServerError, + Message: "There was an error while reading the MIME type of the test archive", + } + } + + if mtype.String() != "application/zip" { + return &sharedDomainErrors.GenericDomainError{ + Code: http.StatusBadRequest, + Message: "Please, make sure to send a ZIP archive", + } + } + + return nil +} + +// ParseMicroserviceError parses the error returned by the archives microservice +func ParseMicroserviceError(resp *http.Response, err error) error { + statusStr := fmt.Sprintf("%d", resp.StatusCode) + isInTwoHundredsGroup := statusStr[0] == '2' + + if err != nil || !isInTwoHundredsGroup { + defaultErrorMessage := "There was an error while requesting the archives microservice" + errorMessage := defaultErrorMessage + + // Decode the body + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return &sharedDomainErrors.GenericDomainError{ + Code: http.StatusBadRequest, + Message: defaultErrorMessage, + } + } + + // Parse the JSON + var responseJSON map[string]interface{} + err = json.Unmarshal(body, &responseJSON) + if err != nil { + return &sharedDomainErrors.GenericDomainError{ + Code: http.StatusBadRequest, + Message: defaultErrorMessage, + } + } + + // Get the error message + msg, ok := responseJSON["message"].(string) + if ok { + errorMessage = msg + } + + // Return the error + return &sharedDomainErrors.GenericDomainError{ + Code: resp.StatusCode, + Message: errorMessage, + } + } + + return nil +} diff --git a/version.json b/version.json index 6ad2102..de1aa91 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.33.0" + "version": "0.37.0" }