diff --git a/app/api/answers/get_answer.feature b/app/api/answers/get_answer.feature index 5d4cc1e1f..f3af16422 100644 --- a/app/api/answers/get_answer.feature +++ b/app/api/answers/get_answer.feature @@ -1,111 +1,449 @@ Feature: Get an answer by id -Background: - Given the database has the following table "groups": - | id | name | type | - | 11 | jdoe | User | - | 13 | Group B | Class | - | 21 | other | User | - | 23 | Group C | Class | - | 24 | jane | User | - | 25 | Group D | Class | - | 26 | Club | Club | - And the database has the following table "users": - | login | temp_user | group_id | first_name | last_name | - | jdoe | 0 | 11 | John | Doe | - | other | 0 | 21 | George | Bush | - | jane | 0 | 24 | Jane | Doe | - And the database has the following table "groups_groups": - | parent_group_id | child_group_id | - | 13 | 11 | - | 13 | 21 | - | 23 | 21 | - | 25 | 24 | - | 26 | 13 | - And the groups ancestors are computed - And the database has the following table "items": - | id | default_language_tag | - | 200 | fr | - | 210 | fr | - And the database has the following table "permissions_generated": - | group_id | item_id | can_view_generated | can_watch_generated | - | 13 | 200 | content | none | - | 23 | 210 | content_with_descendants | none | - | 25 | 210 | none | answer | - And the database has the following table "results": - | attempt_id | participant_id | item_id | - | 1 | 11 | 200 | - | 1 | 13 | 210 | - And the database has the following table "answers": - | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | - | 101 | 11 | 11 | 1 | 200 | Submission | State1 | print(1) | 2017-05-29 06:38:38 | - | 102 | 11 | 13 | 1 | 210 | Submission | State2 | print(2) | 2017-05-29 06:38:38 | - And the database has the following table "gradings": - | answer_id | score | graded_at | - | 101 | 100 | 2018-05-29 06:38:38 | - | 102 | 100 | 2019-05-29 06:38:38 | - And the database has the following table "group_managers": - | group_id | manager_id | can_watch_members | - | 26 | 25 | true | + Background: + Given the database has the following table "items": + | id | default_language_tag | + | @Item1 | fr | + | @Item2 | fr | + And the DB time now is "2024-10-07 20:13:14" + And the time now is "2024-10-07T20:13:14Z" - Scenario: User has can_view>=content on the item and the answers.author_id = authenticated user's self group - Given I am the user with id "11" - When I send a GET request to "/answers/101" + Scenario: User has can_view>=content on the item and the answers.participant_id = authenticated user's self group + Given I am @User + And I can view content of the item @Item1 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @User | 2 | @Item1 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + And the database has the following table "gradings": + | answer_id | score | graded_at | + | 104 | 95 | 2017-05-29 06:38:40 | + When I send a GET request to "/answers/104" Then the response code should be 200 And the response body should be, in JSON: """ { - "id": "101", - "attempt_id": "1", - "participant_id": "11", - "score": 100.0, - "answer": "print(1)", + "id": "104", + "attempt_id": "2", + "participant_id": "@User", + "score": 95, + "answer": "print(3)", "state": "State1", - "created_at": "2017-05-29T06:38:38Z", + "created_at": "2017-05-29T06:38:39Z", "type": "Submission", - "item_id": "200", - "author_id": "11", - "graded_at": "2018-05-29T06:38:38Z" + "item_id": "@Item1", + "author_id": "@Author", + "graded_at": "2017-05-29T06:38:40Z" + } + """ + + Scenario: User has can_view>=content on the item (via an ancestor group) and the answers.participant_id = authenticated user's self group + Given I am @User + And I am a member of the group @ParentGroup + And the group @ParentGroup can view content of the item @Item1 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @User | 2 | @Item1 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@User", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item1", + "author_id": "@Author", + "graded_at": null } """ Scenario: User has can_view>=content on the item and the user is a team member of attempts.participant_id - Given I am the user with id "21" - When I send a GET request to "/answers/102" + Given I am @User + And I can view content of the item @Item2 + And there is a team @Team + And I am a member of the group @Team + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Team | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Team", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: One of the user's teams has can_view>=content on the item and the user is a team member of attempts.participant_id + Given I am @User + And there is a team @Team1 + And there is a team @Team2 + And I am a member of the group @Team1 + And I am a member of the group @Team2 + And the group @Team1 can view content of the item @Item2 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Team2 | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Team2", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: One of the user's teams has can_view>=content (via an ancestor) on the item and the user is a team member of attempts.participant_id + Given I am @User + And there is a team @Team1 + And there is a team @Team2 + And I am a member of the group @Team1 + And I am a member of the group @Team2 + And the group @Team1 is a child of the group @TeamParent + And the group @TeamParent can view content of the item @Item2 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Team2 | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" Then the response code should be 200 And the response body should be, in JSON: """ { - "id": "102", - "attempt_id": "1", - "participant_id": "13", - "score": 100, - "answer": "print(2)", - "state": "State2", - "created_at": "2017-05-29T06:38:38Z", + "id": "104", + "attempt_id": "2", + "participant_id": "@Team2", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", "type": "Submission", - "item_id": "210", - "author_id": "11", - "graded_at": "2019-05-29T06:38:38Z" + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null } """ Scenario: User has can_watch>=answer on the item and can_watch_members on the participant - Given I am the user with id "24" - When I send a GET request to "/answers/102" + Given I am @User + And I have the watch permission set to "answer" on the item @Item2 + And I am a manager of the group @Participant and can watch for submissions from the group and its descendants + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: User has can_watch>=answer (via an ancestor) on the item and can_watch_members (via an ancestor) on the participant + Given I am @User + And I am a member of the group @ChildGroupAbleToWatch + And the group @ChildGroupAbleToWatch is a child of the group @GroupAbleToWatch + And the group @GroupAbleToWatch has the watch permission set to "answer" on the item @Item2 + And I am a member of the group @ManagerGroup + And @Participant is a member of the group @ManagedGroup + And the group @ManagerGroup is a manager of the group @ManagedGroup and can watch for submissions from the group and its descendants + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: User has can_watch>=result on the item and can_watch_members on the participant, and has a validated result on the item + Given I am @User + And I have the watch permission set to "result" on the item @Item2 + And I am a manager of the group @Participant and can watch for submissions from the group and its descendants + And I have a validated result on the item @Item2 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: User has can_watch>=result on the item (via an ancestor group) and can_watch_members on the participant, and has a validated result on the item + Given I am @User + And I am a member of the group @ParentGroup + And the group @ParentGroup has the watch permission set to "result" on the item @Item2 + And I am a manager of the group @Participant and can watch for submissions from the group and its descendants + And I have a validated result on the item @Item2 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" Then the response code should be 200 And the response body should be, in JSON: """ { - "id": "102", - "attempt_id": "1", - "participant_id": "13", - "score": 100, - "answer": "print(2)", - "state": "State2", - "created_at": "2017-05-29T06:38:38Z", + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: User has can_watch>=result on the item and can_watch_members on the participant, and one of the user's teams has a validated result on the item + Given I am @User + And I have the watch permission set to "result" on the item @Item2 + And I am a manager of the group @Participant and can watch for submissions from the group and its descendants + And there is a team @Team1 + And there is a team @Team2 + And I am a member of the group @Team1 + And I am a member of the group @Team2 + And the group @Team2 has a validated result on the item @Item2 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: User has can_watch>=answer on the item and the thread exists + Given I am @User + And I have the watch permission set to "answer" on the item @Item2 + And I am a member of the group @Helper + And there is a thread with "item_id=@Item2,participant_id=@Participant,helper_group_id=@Author,status=closed,latest_update_at={{relativeTimeDB("-1000h")}}" + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario: User has can_watch>=answer (via an ancestor) on the item and the thread exists + Given I am @User + And I am a member of the group @ChildGroupAbleToWatch + And the group @ChildGroupAbleToWatch is a child of the group @GroupAbleToWatch + And the group @GroupAbleToWatch has the watch permission set to "answer" on the item @Item2 + And there is a thread with "item_id=@Item2,participant_id=@Participant,helper_group_id=@Author,status=closed,latest_update_at={{relativeTimeDB("-1000h")}}" + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + + Scenario Outline: User has can_watch>=result on the item and is a thread reader, and has a validated result on the item + Given I am @User + And I have the watch permission set to "result" on the item @Item2 + And I have a validated result on the item @Item2 + And I am a member of the group @Helper + And there is a thread with "item_id=@Item2,participant_id=@Participant,helper_group_id=@Helper,status=,latest_update_at=" + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + Examples: + | thread_status | thread_latest_update_at | + | waiting_for_participant | 2020-05-30 12:00:00 | + | waiting_for_trainer | 2020-05-30 12:00:00 | + | closed | {{relativeTimeDB("-335h59m")}} | + + Scenario Outline: User has can_watch>=result on the item (via an ancestor group) and is a thread reader, and has a validated result on the item + Given I am @User + And I am a member of the group @ParentGroup + And the group @ParentGroup has the watch permission set to "result" on the item @Item2 + And I am a member of the group @Helper + And there is a thread with "item_id=@Item2,participant_id=@Participant,helper_group_id=@Helper,status=,latest_update_at=" + And I have a validated result on the item @Item2 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", + "type": "Submission", + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null + } + """ + Examples: + | thread_status | thread_latest_update_at | + | waiting_for_participant | 2020-05-30 12:00:00 | + | waiting_for_trainer | 2020-05-30 12:00:00 | + | closed | {{relativeTimeDB("-335h59m")}} | + + Scenario Outline: User has can_watch>=result on the item and is a thread reader, and one of the user's teams has a validated result on the item + Given I am @User + And I have the watch permission set to "result" on the item @Item2 + And I am a member of the group @Helper + And there is a thread with "item_id=@Item2,participant_id=@Participant,helper_group_id=@Helper,status=,latest_update_at=" + And there is a team @Team1 + And there is a team @Team2 + And I am a member of the group @Team1 + And I am a member of the group @Team2 + And the group @Team2 has a validated result on the item @Item2 + And the database has the following table "answers": + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 104 | @Author | @Participant | 2 | @Item2 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/104" + Then the response code should be 200 + And the response body should be, in JSON: + """ + { + "id": "104", + "attempt_id": "2", + "participant_id": "@Participant", + "score": null, + "answer": "print(3)", + "state": "State1", + "created_at": "2017-05-29T06:38:39Z", "type": "Submission", - "item_id": "210", - "author_id": "11", - "graded_at": "2019-05-29T06:38:38Z" + "item_id": "@Item2", + "author_id": "@Author", + "graded_at": null } """ + Examples: + | thread_status | thread_latest_update_at | + | waiting_for_participant | 2020-05-30 12:00:00 | + | waiting_for_trainer | 2020-05-30 12:00:00 | + | closed | {{relativeTimeDB("-335h59m")}} | diff --git a/app/api/answers/get_answer.go b/app/api/answers/get_answer.go index beb8a65e1..43cc48d38 100644 --- a/app/api/answers/get_answer.go +++ b/app/api/answers/get_answer.go @@ -10,38 +10,45 @@ import ( // swagger:operation GET /answers/{answer_id} answers answerGet // -// --- -// summary: Get an answer -// description: > -// Returns the answer identified by the given `{answer_id}`. +// --- +// summary: Get an answer +// description: > +// Returns the answer identified by the given `{answer_id}`. // -// - If the user is a participant -// - (s)he should have at least 'content' access rights to the `answers.item_id` and -// - be a member of the `answers.participant_id` team or -// `answers.participant_id` should be equal to the user's self group. +// - If the user is a participant +// - (s)he (or one of his/her teams) should have at least 'content' access rights to the `answers.item_id` and +// - be a member of the `answers.participant_id` team or +// `answers.participant_id` should be equal to the user's self group. // -// - If the user is an observer -// - (s)he should have `can_watch` >= 'answer' permission on the `answers.item_id` and -// - be a manager with `can_watch_members` of an ancestor of `answers.participant_id` group. +// - If the user is an observer (a manager with `can_watch_members` of an ancestor of `answers.participant_id` group) +// - (s)he should have `can_watch` >= 'answer' permission on the `answers.item_id` OR +// - `can_watch` >= 'result' permission on the `answers.item_id` together with a validated result +// (personally or as a team) on the `answers.item_id`. // -// If any of the preconditions fails, the 'forbidden' error is returned. -// parameters: -// - name: answer_id -// in: path -// type: integer -// required: true -// format: int64 -// responses: -// "200": -// "$ref": "#/responses/itemAnswerGetResponse" -// "400": -// "$ref": "#/responses/badRequestResponse" -// "401": -// "$ref": "#/responses/unauthorizedResponse" -// "403": -// "$ref": "#/responses/forbiddenResponse" -// "500": -// "$ref": "#/responses/internalErrorResponse" +// - If the user is a thread reader (when the thread for the `answers.participant_id`-`answers.item_id` pair exists) +// - (s)he should have `can_watch` >= 'answer' permission on the `answers.item_id` OR +// - (s)he should be a descendant of the thread's `helper_group_id` and have `can_watch` >= 'result' permission +// on the `answers.item_id` together with a validated result (personally or as a team) on the `answers.item_id` +// while the thread should be active or closed less than 2 weeks ago. +// +// If any of the preconditions fails, the 'forbidden' error is returned. +// parameters: +// - name: answer_id +// in: path +// type: integer +// required: true +// format: int64 +// responses: +// "200": +// "$ref": "#/responses/itemAnswerGetResponse" +// "400": +// "$ref": "#/responses/badRequestResponse" +// "401": +// "$ref": "#/responses/unauthorizedResponse" +// "403": +// "$ref": "#/responses/forbiddenResponse" +// "500": +// "$ref": "#/responses/internalErrorResponse" func (srv *Service) getAnswer(rw http.ResponseWriter, httpReq *http.Request) service.APIError { answerID, err := service.ResolveURLQueryPathInt64Field(httpReq, "answer_id") if err != nil { @@ -52,35 +59,107 @@ func (srv *Service) getAnswer(rw http.ResponseWriter, httpReq *http.Request) ser var result []map[string]interface{} store := srv.GetStore(httpReq) - usersGroupsQuery := store.ActiveGroupGroups().WhereUserIsMember(user).Select("parent_group_id") + + userAndHisTeamsSubQuery := store.Raw("SELECT id FROM ? `teams` UNION ALL SELECT ?", + store.ActiveGroupGroups(). + WhereUserIsMember(user). + Joins("JOIN `groups` ON groups.id = groups_groups_active.parent_group_id AND groups.type='Team'"). + Select("groups.id").SubQuery(), + user.GroupID).SubQuery() // a participant should have at least 'content' access to the answers.item_id - participantItemPerms := store.Permissions().MatchingUserAncestors(user). + userHasViewContentPermOnItemSubQuery := store.Permissions().MatchingUserAncestors(user). WherePermissionIsAtLeast("view", "content"). Where("permissions.item_id = answers.item_id"). - Select("1").Limit(1) - // an observer should have 'can_watch'>='answer' permission on the answers.item_id - observerItemPerms := store.Permissions().MatchingUserAncestors(user). + Select("1").Limit(1).SubQuery() + // or a participant's team should have at least 'content' access to the answers.item_id + userTeamHasViewContentPermOnItemSubQuery := store.Permissions(). + Joins("JOIN `groups_ancestors_active` ON groups_ancestors_active.ancestor_group_id = permissions.group_id"). + Joins("JOIN `groups_groups_active` ON groups_groups_active.parent_group_id = groups_ancestors_active.child_group_id"). + Where("groups_groups_active.child_group_id = ?", user.GroupID). + Joins("JOIN `groups` ON groups.id = groups_groups_active.parent_group_id AND groups.type='Team'"). + WherePermissionIsAtLeast("view", "content"). + Where("permissions.item_id = answers.item_id"). + Select("1").Limit(1).SubQuery() + + // an observer/thread viewer should have 'can_watch'>='answer' permission on the answers.item_id + userHasCanWatchAnswerPermOnItemSubQuery := store.Permissions().MatchingUserAncestors(user). WherePermissionIsAtLeast("watch", "answer"). Where("permissions.item_id = answers.item_id"). - Select("1").Limit(1) + Select("1").Limit(1).SubQuery() + // or an observer/helper should have 'can_watch'>='result' permission on the answers.item_id + userHasCanWatchResultPermOnItemSubQuery := store.Permissions().MatchingUserAncestors(user). + WherePermissionIsAtLeast("watch", "result"). + Where("permissions.item_id = answers.item_id"). + Select("1").Limit(1).SubQuery() + // and an observer/helper or his team should have a validated result on the answers.item_id + userOrHisTeamHasValidatedResultOnItemSubQuery := store.Results(). + Where("results.item_id = answers.item_id"). + Where("results.validated"). + Where("results.participant_id IN (SELECT id FROM user_and_his_teams)"). + Select("1").Limit(1).SubQuery() + // an observer should be able to watch the participant - observerParticipantPerms := store.ActiveGroupAncestors().ManagedByUser(user). + userIsAManagerThatCanWatchMembersSubQuery := store.ActiveGroupAncestors().ManagedByUser(user). Where("groups_ancestors_active.child_group_id = answers.participant_id"). Where("can_watch_members"). - Select("1").Limit(1) + Select("1").Limit(1).SubQuery() + + // the thread should exist to allow thread viewers with 'can_watch'>='answer' permission to view the answer + theThreadExistsSubQuery := store.Threads(). + Where("threads.participant_id = answers.participant_id"). + Where("threads.item_id = answers.item_id"). + Select("1").Limit(1).SubQuery() - err = store.Answers(). - WithGradings(). - ByID(answerID). - // 1) the user is the participant or a member of the participant group able to view the item, - // 2) or an observer with required permissions - Where(` - (? AND (answers.participant_id = ? OR answers.participant_id IN ?)) OR - (? AND ?)`, - participantItemPerms.SubQuery(), user.GroupID, usersGroupsQuery.SubQuery(), - observerItemPerms.SubQuery(), observerParticipantPerms.SubQuery()). + // a helper should be an ancestor of the thread's helper group and the thread should be active or closed less than 2 weeks ago + userIsAHelperAndTheThreadHasNotBeenExpiredSubQuery := store.Threads(). + Where("threads.participant_id = answers.participant_id"). + Where("threads.item_id = answers.item_id"). + Where("threads.status IN ('waiting_for_participant', 'waiting_for_trainer') OR threads.latest_update_at > NOW() - INTERVAL 2 WEEK"). + Joins(` + JOIN groups_ancestors_active + ON groups_ancestors_active.child_group_id = ? AND groups_ancestors_active.ancestor_group_id = threads.helper_group_id`, + user.GroupID). + Select("1").Limit(1).SubQuery() + + err = store.Raw("WITH user_and_his_teams AS ? ?", + userAndHisTeamsSubQuery, + store.Answers(). + WithGradings(). + ByID(answerID). + // 1) the user is the participant or a member of the participant team able to view the item, + // 2) or an observer with required permissions + // 3) or a thread viewer with required permissions + Where(` + ((? OR ?) AND answers.participant_id IN (SELECT id from user_and_his_teams)) OR + (? AND (? OR ?)) OR + (? AND ? AND (? OR ?))`, + /* ( */ + /* ( */ + userHasViewContentPermOnItemSubQuery /* OR */, userTeamHasViewContentPermOnItemSubQuery, + /* ) */ + /* AND [the user/(his team) is the participant] */ + /* ) */ + /* OR */ + /* ( */ + userHasCanWatchAnswerPermOnItemSubQuery, + /* AND */ + /* ( */ + userIsAManagerThatCanWatchMembersSubQuery /* OR */, theThreadExistsSubQuery, + /* ) */ + /* ) */ + /* OR */ + /* ( */ + userHasCanWatchResultPermOnItemSubQuery, + /* AND */ + userOrHisTeamHasValidatedResultOnItemSubQuery, + /* AND */ + /* ( */ + userIsAManagerThatCanWatchMembersSubQuery /* OR */, userIsAHelperAndTheThreadHasNotBeenExpiredSubQuery, + /* ) */). + SubQuery()). ScanIntoSliceOfMaps(&result).Error() + service.MustNotBeError(err) if len(result) == 0 { return service.InsufficientAccessRightsError diff --git a/app/api/answers/get_answer.robustness.feature b/app/api/answers/get_answer.robustness.feature index d91d82977..e0a30585a 100644 --- a/app/api/answers/get_answer.robustness.feature +++ b/app/api/answers/get_answer.robustness.feature @@ -35,9 +35,10 @@ Feature: Get user's answer by id | 1 | 11 | | 1 | 13 | And the database has the following table "results": - | attempt_id | participant_id | item_id | - | 1 | 11 | 200 | - | 1 | 13 | 210 | + | attempt_id | participant_id | item_id | validated_at | + | 1 | 11 | 200 | 2017-05-29 12:00:00 | + | 1 | 13 | 210 | 2017-05-29 12:00:00 | + | 1 | 16 | 200 | null | And the database has the following table "answers": | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | | 101 | 11 | 11 | 1 | 200 | Submission | State101 | print(1) | 2017-05-29 06:38:38 | @@ -53,6 +54,8 @@ Feature: Get user's answer by id | 13 | 15 | false | | 13 | 16 | true | | 13 | 17 | true | + And the DB time now is "2024-10-07 20:13:14" + And the time now is "2024-10-07T20:13:14Z" Scenario: Wrong answer_id Given I am the user with id "11" @@ -96,12 +99,72 @@ Feature: Get user's answer by id Then the response code should be 403 And the response error message should contain "Insufficient access rights" - Scenario: No access rights to the answer (the user is an observer with can_watch_members, but with can_watch=answer, but the thread doesn't exist) + Given I am @User + And I have the watch permission set to "answer" on the item 200 + And there is a user @Participant + And there is a thread with "item_id=200,participant_id=@User,helper_group_id=@Helper,status=closed,latest_update_at={{relativeTimeDBMs("-1h")}}" + And there is a thread with "item_id=210,participant_id=@Participant,helper_group_id=@Helper,status=waiting_for_participant" + And the database table "answers" has also the following rows: + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 105 | @Participant | @Participant | 2 | 200 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/105" + Then the response code should be 403 + And the response error message should contain "Insufficient access rights" + + Scenario: No access rights to the answer (the user is from the helper group with can_watch>=result, has a validated result, but the thread has been expired) + Given I am @User + And I am a member of the group @Helper + And I have the watch permission set to "result" on the item 200 + And I have a validated result on the item 200 + And there is a user @Participant + And there is a thread with "item_id=200,participant_id=@Participant,helper_group_id=@Helper,status=closed,latest_update_at={{relativeTimeDB("-336h")}}" + And the database table "answers" has also the following rows: + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 105 | @Participant | @Participant | 2 | 200 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/105" + Then the response code should be 403 + And the response error message should contain "Insufficient access rights" + + Scenario: No access rights to the answer (the user is from the helper group with can_watch>=result, but has a not validated result, although the thread has not been expired) + Given I am @User + And I am a member of the group @Helper + And I have the watch permission set to "result" on the item 200 + And the database table "results" has also the following rows: + | attempt_id | participant_id | item_id | validated_at | + | 2 | @User | 200 | null | + And there is a user @Participant + And there is a thread with "item_id=200,participant_id=@Participant,helper_group_id=@Helper,status=waiting_for_participant" + And the database table "answers" has also the following rows: + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 105 | @Participant | @Participant | 2 | 200 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/105" + Then the response code should be 403 + And the response error message should contain "Insufficient access rights" + + Scenario: No access rights to the answer (the user is from the helper group with can_watch>=result and has a validated results, + but there is no thread for the participant-item pair for this helper group) + Given I am @User + And I am a member of the group @Helper + And I have the watch permission set to "result" on the item 200 + And I have a validated result on the item 200 + And there is a user @Participant + And there is a thread with "item_id=200,participant_id=@User,helper_group_id=@Helper,status=waiting_for_participant" + And there is a thread with "item_id=200,participant_id=@Participant,helper_group_id=@Participant,status=waiting_for_participant" + And there is a thread with "item_id=210,participant_id=@Participant,helper_group_id=@Helper,status=waiting_for_participant" + And the database table "answers" has also the following rows: + | id | author_id | participant_id | attempt_id | item_id | type | state | answer | created_at | + | 105 | @Participant | @Participant | 2 | 200 | Submission | State1 | print(3) | 2017-05-29 06:38:39 | + When I send a GET request to "/answers/105" + Then the response code should be 403 + And the response error message should contain "Insufficient access rights" + Scenario: No answers Given I am the user with id "11" When I send a GET request to "/answers/100" diff --git a/app/doc/responses.go b/app/doc/responses.go index 57090f24a..663e222a8 100644 --- a/app/doc/responses.go +++ b/app/doc/responses.go @@ -196,8 +196,7 @@ type itemAnswerGetResponse struct { AuthorID int64 `json:"author_id,string"` // required:true ItemID int64 `json:"item_id,string"` - // Nullable - // format:integer + // format:int64 // required:true AttemptID *string `json:"attempt_id,string"` // Can be `null` when there is no applicable existing answer for the user. @@ -206,18 +205,14 @@ type itemAnswerGetResponse struct { // required:true // enum:Submission,Saved,Current Type string `json:"type"` - // Nullable // required:true State *string `json:"state"` - // Nullable // required:true Answer *string `json:"answer"` // required:true CreatedAt time.Time `json:"created_at"` - // Nullable // required:true Score *float32 `json:"score"` - // Nullable // required:true GradedAt *time.Time `json:"graded_at"` } @@ -236,7 +231,6 @@ type itemEnterResponse struct { Success bool `json:"success"` // required: true Data struct { - // Nullable // pattern: ^\d{1,3}:[0-5]?\d:[0-5]?\d$ // example: 838:59:59 // required: true diff --git a/testhelpers/app_language_groups.go b/testhelpers/app_language_groups.go index e9140191c..d8e5b07ea 100644 --- a/testhelpers/app_language_groups.go +++ b/testhelpers/app_language_groups.go @@ -15,6 +15,7 @@ import ( func (ctx *TestContext) registerFeaturesForGroups(s *godog.ScenarioContext) { s.Step(`^there are the following groups:$`, ctx.ThereAreTheFollowingGroups) s.Step(`^there is a group (@\w+)$`, ctx.ThereIsAGroup) + s.Step(`^there is a team (@\w+)$`, ctx.ThereIsATeam) s.Step(`^I am a member of the group (@\w+)$`, ctx.IAmAMemberOfTheGroup) s.Step(`^I am a member of the group with id "([^"]*)"$`, ctx.IAmAMemberOfTheGroupWithID) s.Step(`^(@\w+) is a member of the group (@\w+)$`, ctx.UserIsAMemberOfTheGroup) @@ -38,7 +39,7 @@ func (ctx *TestContext) getGroupPrimaryKey(groupID int64) map[string]string { } // addGroup adds a group to the database. -func (ctx *TestContext) addGroup(group string) { +func (ctx *TestContext) addGroup(group, groupType string) { groupID := ctx.getIDOfReference(group) primaryKey := ctx.getGroupPrimaryKey(groupID) @@ -57,7 +58,7 @@ func (ctx *TestContext) addGroup(group string) { {Cells: []*messages.PickleTableCell{ {Value: strconv.FormatInt(groupID, 10)}, {Value: "Group " + referenceToName(group)}, - {Value: "Class"}, + {Value: groupType}, {Value: "none"}, {Value: "null"}, {Value: "false"}, @@ -127,9 +128,16 @@ func (ctx *TestContext) ThereAreTheFollowingGroups(groups *godog.Table) error { return nil } -// ThereIsAGroup creates a new group. +// ThereIsAGroup creates a new group (type=Class). func (ctx *TestContext) ThereIsAGroup(group string) error { - ctx.addGroup(group) + ctx.addGroup(group, "Class") + + return nil +} + +// ThereIsATeam creates a new team. +func (ctx *TestContext) ThereIsATeam(group string) error { + ctx.addGroup(group, "Team") return nil } diff --git a/testhelpers/feature_context.go b/testhelpers/feature_context.go index e56e22574..862d080dd 100644 --- a/testhelpers/feature_context.go +++ b/testhelpers/feature_context.go @@ -63,7 +63,11 @@ func InitializeScenario(s *godog.ScenarioContext) { s.Step(`^there are the following item relations:$`, ctx.ThereAreTheFollowingItemRelations) s.Step(`^I can view (none|info|content|content_with_descendants|solution) of the item (.+)$`, ctx.IHaveViewPermissionOnItem) + s.Step(`^the group (\@\w+) can view (none|info|content|content_with_descendants|solution) of the item (.+)$`, + ctx.GroupHasViewPermissionOnItem) s.Step(`^I have the watch permission set to "(none|result|answer|answer_with_grant)" on the item (.+)$`, ctx.IHaveWatchPermissionOnItem) + s.Step(`^the group (.+) has the watch permission set to "(none|result|answer|answer_with_grant)" on the item (.+)$`, + ctx.GroupHasWatchPermissionOnItem) s.Step(`^I can request help to the group with id "([^"]*)" on the item with id "([^"]*)"$`, ctx.ICanRequestHelpToTheGroupWithIDOnTheItemWithID) @@ -72,6 +76,7 @@ func InitializeScenario(s *godog.ScenarioContext) { s.Step(`^there are the following validated results:$`, ctx.ThereAreTheFollowingValidatedResults) s.Step(`^I have a validated result on the item (.+)$`, ctx.IHaveValidatedResultOnItem) + s.Step(`^the group (.+) has a validated result on the item (.+)$`, ctx.GroupHasValidatedResultOnItem) s.Step(`^there are the following threads:$`, ctx.ThereAreTheFollowingThreads) s.Step(`^there is a thread with "(.*)"$`, ctx.ThereIsAThreadWith)