Skip to content

Commit

Permalink
feat: Add criteria to rubric objective (#84)
Browse files Browse the repository at this point in the history
* refactor(rubrics): Create functions to verify rubrics ownership

* fix(rubrics): Use separated views to verify objectives and criteria ownership

* docs(openapi): Update spec

* feat(rubrics): Create endpoint to add criteria to a rubric objective

* fix(rubrics): Order objectives and criteria by creation date

* chore: Clean tests cache before running tests

* test(accounts): Register a second teacher account by default

* test(rubrics): Ensure teacher can add criteria to rubric objective

* chore(db): Update down migration

* chore(deps): Bump dependencies
  • Loading branch information
PedroChaparro authored Oct 13, 2023
1 parent 7d8b2f8 commit 2bca229
Show file tree
Hide file tree
Showing 17 changed files with 404 additions and 13 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ delete_pakage:
rm -rf src/$$name;

coverage:
go clean -testcache; \
go test -coverpkg ./... -coverprofile coverage.txt __tests__/integration/*.go; \
go tool cover -html=coverage.txt -o coverage.html;
24 changes: 22 additions & 2 deletions __tests__/integration/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ var (

registeredTeacherEmail string
registeredTeacherPass string

secondRegisteredTeacherEmail string
secondRegisteredTeacherPass string
)

type GenericTestCase struct {
Expand Down Expand Up @@ -59,7 +62,7 @@ func setupRouter() {

func registerBaseAccounts() {
registerBaseStudent()
registerBaseTeacher()
registerBaseTeachers()
}

func registerBaseStudent() {
Expand All @@ -80,7 +83,8 @@ func registerBaseStudent() {
registeredStudentPass = studentPassword
}

func registerBaseTeacher() {
func registerBaseTeachers() {
// Register the first teacher
teacherEmail := "[email protected]"
teacherPassword := "judy/password/2023"

Expand All @@ -95,6 +99,22 @@ func registerBaseTeacher() {

registeredTeacherEmail = teacherEmail
registeredTeacherPass = teacherPassword

// Register the second teacher
secondTeacherEmail := "[email protected]"
secondTeacherPassword := "trofim/password/2023"

code = RegisterTeacherAccount(requests.RegisterTeacherRequest{
FullName: "Trofim Vijay",
Email: secondTeacherEmail,
Password: secondTeacherPassword,
})
if code != http.StatusCreated {
panic("Error registering base teacher")
}

secondRegisteredTeacherEmail = secondTeacherEmail
secondRegisteredTeacherPass = secondTeacherPassword
}

// --- Helpers ---
Expand Down
125 changes: 125 additions & 0 deletions __tests__/integration/rubrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,128 @@ func AddObjectiveToRubric(cookie *http.Cookie, rubricUUID string, payload map[st

return ParseJsonResponse(w.Body), w.Code
}

func TestAddCriteriaToObjective(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)
firstTeacherCookie := w.Result().Cookies()[0]

// Create a rubric
response, status := CreateRubric(firstTeacherCookie, map[string]interface{}{
"name": "Rubric 1",
})
c.Equal(http.StatusCreated, status)
firstTeacherRubricUUID := response["uuid"].(string)

// Create an objective
response, status = AddObjectiveToRubric(firstTeacherCookie, firstTeacherRubricUUID, map[string]interface{}{
"description": "Objective 1",
})
c.Equal(http.StatusCreated, status)
firstTeacherObjectiveUUID := response["uuid"].(string)

// Login as the second teacher
w, r = PrepareRequest("POST", "/api/v1/session/login", map[string]interface{}{
"email": secondRegisteredTeacherEmail,
"password": secondRegisteredTeacherPass,
})
router.ServeHTTP(w, r)
secondTeacherCookie := w.Result().Cookies()[0]

// Create a rubric
response, status = CreateRubric(secondTeacherCookie, map[string]interface{}{
"name": "Rubric 2",
})
c.Equal(http.StatusCreated, status)
secondTeacherRubricUUID := response["uuid"].(string)

// Create an objective
response, status = AddObjectiveToRubric(secondTeacherCookie, secondTeacherRubricUUID, map[string]interface{}{
"description": "Objective 2",
})
c.Equal(http.StatusCreated, status)
secondTeacherObjectiveUUID := response["uuid"].(string)

// Test cases
testCases := []GenericTestCase{
GenericTestCase{
Payload: map[string]interface{}{
"objectiveUUID": "not-valid-uuid",
"description": "Criteria 1",
"weight": 5.00,
},
ExpectedStatusCode: http.StatusBadRequest,
},
GenericTestCase{
Payload: map[string]interface{}{
"objectiveUUID": firstTeacherObjectiveUUID,
"description": "Criteria 1",
"weight": 5.00,
},
ExpectedStatusCode: http.StatusForbidden,
},
GenericTestCase{
Payload: map[string]interface{}{
"objectiveUUID": secondTeacherObjectiveUUID,
"description": "short",
"weight": 5.00,
},
ExpectedStatusCode: http.StatusBadRequest,
},
GenericTestCase{
Payload: map[string]interface{}{
"objectiveUUID": "adc73ae3-80ad-45d3-ae23-bd81e6e0b805",
"description": "Criteria 1",
"weight": 5.00,
},
ExpectedStatusCode: http.StatusNotFound,
},
GenericTestCase{
Payload: map[string]interface{}{
"objectiveUUID": secondTeacherObjectiveUUID,
"description": "Criteria 1",
"weight": 5.00,
},
ExpectedStatusCode: http.StatusCreated,
},
}

for _, testCase := range testCases {
response, status := AddCriteriaToObjective(secondTeacherCookie, testCase.Payload["objectiveUUID"].(string), testCase.Payload)
c.Equal(testCase.ExpectedStatusCode, status)

if testCase.ExpectedStatusCode == http.StatusCreated {
c.NotEmpty(response["uuid"])
c.NotEmpty(response["message"])
}
}

// Get rubric
response, status = GetRubricByUUID(secondTeacherCookie, secondTeacherRubricUUID)
c.Equal(http.StatusOK, status)

rubric := response["rubric"].(map[string]interface{})
c.Equal(2, len(rubric["objectives"].([]interface{})))

objective := rubric["objectives"].([]interface{})[1].(map[string]interface{})
c.Equal(1, len(objective["criteria"].([]interface{})))

criteria := objective["criteria"].([]interface{})[0].(map[string]interface{})
c.Equal("Criteria 1", criteria["description"])
c.NotEmpty(criteria["uuid"])
c.NotEmpty(criteria["weight"])
}

func AddCriteriaToObjective(cookie *http.Cookie, objectiveUUID string, payload map[string]interface{}) (response map[string]interface{}, status int) {
w, r := PrepareRequest("POST", "/api/v1/rubrics/objectives/"+objectiveUUID+"/criteria", payload)
r.AddCookie(cookie)
router.ServeHTTP(w, r)

return ParseJsonResponse(w.Body), w.Code
}
4 changes: 2 additions & 2 deletions docs/openapi/spec.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,7 @@ paths:
schema:
$ref: "#/components/schemas/default_error_response"

/rubrics/{objective_uuid}/criteria:
/rubrics/objectives/{objective_uuid}/criteria:
post:
tags:
- Rubrics
Expand Down Expand Up @@ -1498,7 +1498,7 @@ components:
description:
type: string
example: "Se evidencia desarrollo de, al menos, el 75% de los métodos solicitados para la estructura de datos planteada"
grade:
weight:
type: number
example: 0.20

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
Expand All @@ -38,7 +38,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.16.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ 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.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
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/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
Expand Down Expand Up @@ -188,6 +190,8 @@ golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
4 changes: 4 additions & 0 deletions sql/migrations/20230920232901_init.down.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ DROP VIEW IF EXISTS courses_has_users_view;

DROP VIEW IF EXISTS users_with_creator;

DROP VIEW IF EXISTS objectives_owners;

DROP VIEW IF EXISTS criteria_owners;

-- ## Tables
DROP TABLE IF EXISTS grade_has_criteria;

Expand Down
27 changes: 25 additions & 2 deletions sql/migrations/20230920232901_init.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ 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
);

CREATE TABLE IF NOT EXISTS criteria (
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
"objective_id" UUID NOT NULL REFERENCES objectives(id),
"description" VARCHAR(510) NOT NULL,
"weight" DECIMAL(5, 2) NOT NULL
"weight" DECIMAL(5, 2) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS laboratories (
Expand Down Expand Up @@ -183,6 +185,27 @@ FROM
INNER JOIN courses ON courses_has_users.course_id = courses.id
INNER JOIN colors ON courses.color_id = colors.id;

--- ### Objectives
CREATE
OR REPLACE VIEW objectives_owners AS
SELECT
objectives.id AS objective_id,
rubrics.teacher_id
FROM
objectives
INNER JOIN rubrics ON objectives.rubric_id = rubrics.id;

--- ### Criteria
CREATE
OR REPLACE VIEW criteria_owners AS
SELECT
criteria.id AS criteria_id,
rubrics.teacher_id
FROM
criteria
INNER JOIN objectives ON criteria.objective_id = objectives.id
INNER JOIN rubrics ON objectives.rubric_id = rubrics.id;

-- ## Triggers
--- ### Update created_by on users
CREATE
Expand Down
27 changes: 22 additions & 5 deletions src/rubrics/application/use_cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,12 @@ func (useCases *RubricsUseCases) GetRubricByUUID(dto *dtos.GetRubricDto) (rubric
}

func (useCases *RubricsUseCases) AddObjectiveToRubric(dto *dtos.AddObjectiveToRubricDTO) (objectiveUUID string, err error) {
// Get the rubric
rubric, err := useCases.RubricsRepository.GetByUUID(dto.RubricUUID)
// Check if the rubric belongs to the teacher
teacherOwnsRubric, err := useCases.RubricsRepository.DoesTeacherOwnRubric(dto.TeacherUUID, dto.RubricUUID)
if err != nil {
return "", err
}

// Check if the rubric belongs to the teacher
if rubric.TeacherUUID != dto.TeacherUUID {
if !teacherOwnsRubric {
return "", &errors.TeacherDoesNotOwnsRubric{}
}

Expand All @@ -64,3 +62,22 @@ func (useCases *RubricsUseCases) AddObjectiveToRubric(dto *dtos.AddObjectiveToRu

return objectiveUUID, nil
}

func (useCases *RubricsUseCases) AddCriteriaToObjective(dto *dtos.AddCriteriaToObjectiveDTO) (criteriaUUID string, err error) {
// Check if the objective belongs to a rubric that belongs to the teacher
teacherOwnsObjective, err := useCases.RubricsRepository.DoesTeacherOwnObjective(dto.TeacherUUID, dto.ObjectiveUUID)
if err != nil {
return "", err
}
if !teacherOwnsObjective {
return "", &errors.TeacherDoesNotOwnsRubric{}
}

// Add the criteria
criteriaUUID, err = useCases.RubricsRepository.AddCriteriaToObjective(dto)
if err != nil {
return "", err
}

return criteriaUUID, nil
}
5 changes: 5 additions & 0 deletions src/rubrics/domain/definitions/rubrics_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ type RubricsRepository interface {
GetByUUID(uuid string) (rubric *entities.Rubric, err error)
GetAllCreatedByTeacher(teacherUUID string) (rubrics []*dtos.CreatedRubricDTO, err error)

DoesTeacherOwnRubric(teacherUUID string, rubricUUID string) (bool, error)
DoesTeacherOwnObjective(teacherUUID string, objectiveUUID string) (bool, error)
DoesTeacherOwnCriteria(teacherUUID string, criteriaUUID string) (bool, error)

AddObjectiveToRubric(rubricUUID string, objectiveDescription string) (objectiveUUID string, err error)
AddCriteriaToObjective(dto *dtos.AddCriteriaToObjectiveDTO) (criteriaUUID string, err error)
}
8 changes: 8 additions & 0 deletions src/rubrics/domain/dtos/add_criteria_to_objective_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dtos

type AddCriteriaToObjectiveDTO struct {
TeacherUUID string
ObjectiveUUID string
CriteriaDescription string
CriteriaWeight float64
}
13 changes: 13 additions & 0 deletions src/rubrics/domain/errors/criteria_not_found_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package errors

import "net/http"

type CriteriaNotFoundError struct{}

func (err *CriteriaNotFoundError) Error() string {
return "Rubric criteria not found"
}

func (err *CriteriaNotFoundError) StatusCode() int {
return http.StatusNotFound
}
13 changes: 13 additions & 0 deletions src/rubrics/domain/errors/objective_not_found_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package errors

import "net/http"

type ObjectiveNotFoundError struct{}

func (err *ObjectiveNotFoundError) Error() string {
return "Rubric objective not found"
}

func (err *ObjectiveNotFoundError) StatusCode() int {
return http.StatusNotFound
}
Loading

0 comments on commit 2bca229

Please sign in to comment.