diff --git a/controller/adapter.go b/controller/adapter.go index 26d03669..b4986edb 100644 --- a/controller/adapter.go +++ b/controller/adapter.go @@ -211,7 +211,7 @@ func questionnaire2QuestionnaireDetail(questionnaires model.Questionnaires, admi return res, nil } -func respondentDetail2Response(ctx echo.Context, respondentDetail model.RespondentDetail) (openapi.Response, error) { +func respondentDetail2ResponseWithMetadata(ctx echo.Context, respondentDetail model.RespondentDetail, respondent *string, isAnonymous bool) (openapi.Response, error) { oResponseBodies := []openapi.ResponseBody{} for _, r := range respondentDetail.Responses { oResponseBody := openapi.ResponseBody{} @@ -318,13 +318,6 @@ func respondentDetail2Response(ctx echo.Context, respondentDetail model.Responde oResponseBodies = append(oResponseBodies, oResponseBody) } - isAnonymous, err := model.NewQuestionnaire().GetResponseIsAnonymousByQuestionnaireID(ctx.Request().Context(), respondentDetail.QuestionnaireID) - if err != nil { - ctx.Logger().Errorf("failed to get response is anonymous: %+v", err) - return openapi.Response{}, err - } - - respondent := &respondentDetail.TraqID if isAnonymous { respondent = nil } @@ -343,6 +336,21 @@ func respondentDetail2Response(ctx echo.Context, respondentDetail model.Responde return res, nil } +func respondentDetail2Response(ctx echo.Context, respondentDetail model.RespondentDetail) (openapi.Response, error) { + isAnonymous, err := model.NewQuestionnaire().GetResponseIsAnonymousByQuestionnaireID(ctx.Request().Context(), respondentDetail.QuestionnaireID) + if err != nil { + ctx.Logger().Errorf("failed to get response is anonymous: %+v", err) + return openapi.Response{}, err + } + + respondent := &respondentDetail.TraqID + if isAnonymous { + respondent = nil + } + + return respondentDetail2ResponseWithMetadata(ctx, respondentDetail, respondent, isAnonymous) +} + func responseBody2ResponseMetas(body []openapi.NewResponseBody, questions []model.Questions) ([]*model.ResponseMeta, error) { res := []*model.ResponseMeta{} diff --git a/controller/questionnaire.go b/controller/questionnaire.go index 2907402f..90025f44 100644 --- a/controller/questionnaire.go +++ b/controller/questionnaire.go @@ -1110,8 +1110,8 @@ func (q *Questionnaire) PostQuestionnaireResponse(c echo.Context, questionnaireI responseMetas, err := responseBody2ResponseMetas(params.Body, questions) if err != nil { - c.Logger().Errorf("failed to convert response body to response metas: %+v", err) - return res, echo.NewHTTPError(http.StatusInternalServerError, err) + c.Logger().Infof("invalid response body: %+v", err) + return res, echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid response body: %w", err)) } // validationでチェック diff --git a/controller/questionnaire_test.go b/controller/questionnaire_test.go index a80a0978..22034229 100644 --- a/controller/questionnaire_test.go +++ b/controller/questionnaire_test.go @@ -212,6 +212,9 @@ func TestGetQuestionnaires(t *testing.T) { assertion := assert.New(t) + uniqueSearchTitle := fmt.Sprintf("search test %d", time.Now().UnixNano()) + specialTargetUser := fmt.Sprintf("stgq%d", time.Now().UnixNano()) + questionnaire := sampleQuestionnaire e := echo.New() body, err := json.Marshal(questionnaire) @@ -224,7 +227,7 @@ func TestGetQuestionnaires(t *testing.T) { require.NoError(t, err) questionnaire = sampleQuestionnaire - questionnaire.Title = "search test" + questionnaire.Title = uniqueSearchTitle e = echo.New() body, err = json.Marshal(questionnaire) require.NoError(t, err) @@ -236,7 +239,7 @@ func TestGetQuestionnaires(t *testing.T) { require.NoError(t, err) questionnaire = sampleQuestionnaire - questionnaire.Title = "search test" + questionnaire.Title = uniqueSearchTitle e = echo.New() body, err = json.Marshal(questionnaire) require.NoError(t, err) @@ -260,7 +263,7 @@ func TestGetQuestionnaires(t *testing.T) { require.NoError(t, err) questionnaire = sampleQuestionnaire - questionnaire.Target.Users = []string{"specialTargetUser"} + questionnaire.Target.Users = []string{specialTargetUser} e = echo.New() body, err = json.Marshal(questionnaire) require.NoError(t, err) @@ -293,7 +296,7 @@ func TestGetQuestionnaires(t *testing.T) { sortTitleDesc := (openapi.SortInQuery)("-title") sortModifiedAt := (openapi.SortInQuery)("modified_at") sortModifiedAtDesc := (openapi.SortInQuery)("-modified_at") - searchTest := (openapi.SearchInQuery)("search test") + searchTest := openapi.SearchInQuery(uniqueSearchTitle) largePageNum := 100000000 constTrue := true @@ -435,7 +438,7 @@ func TestGetQuestionnaires(t *testing.T) { { description: "only targeting by me special target user", args: args{ - userID: "specialTargetUser", + userID: specialTargetUser, params: openapi.GetQuestionnairesParams{ OnlyTargetingMe: &constTrue, }, diff --git a/controller/response.go b/controller/response.go index 4041b103..596d2195 100644 --- a/controller/response.go +++ b/controller/response.go @@ -50,13 +50,18 @@ func NewResponse( } func (r *Response) GetMyResponses(ctx echo.Context, params openapi.GetMyResponsesParams, userID string) (openapi.ResponsesWithQuestionnaireInfo, error) { - res := openapi.ResponsesWithQuestionnaireInfo{} + res := openapi.ResponsesWithQuestionnaireInfo{ + ResponseGroups: []openapi.ResponseWithQuestionnaireInfoItem{}, + } - var sort string - if params.Sort == nil { - sort = "" + var pageNum int + if params.Page == nil { + pageNum = 1 } else { - sort = string(*params.Sort) + pageNum = int(*params.Page) + } + if pageNum < 1 { + pageNum = 1 } var questionnaireIDs []int if params.QuestionnaireIDs == nil { @@ -64,74 +69,48 @@ func (r *Response) GetMyResponses(ctx echo.Context, params openapi.GetMyResponse } else { questionnaireIDs = *params.QuestionnaireIDs } - responsesID, err := r.IRespondent.GetMyResponseIDs(ctx.Request().Context(), sort, userID, questionnaireIDs, params.IsDraft) - if err != nil { - ctx.Logger().Errorf("failed to get my responses ID: %+v", err) - return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire responses: %w", err)) - } - responseLists := make(map[int][]openapi.Response) - var responseQuestionnaireIDs []int - - for _, responseID := range responsesID { - responseDetail, err := r.IRespondent.GetRespondentDetail(ctx.Request().Context(), responseID) - if err != nil { - ctx.Logger().Errorf("failed to get respondent detail: %+v", err) - return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get respondent detail: %w", err)) - } - - response, err := respondentDetail2Response(ctx, responseDetail) - if err != nil { - ctx.Logger().Errorf("failed to convert respondent detail into response: %+v", err) - return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to convert respondent detail into response: %w", err)) - } - - tmp := openapi.Response{ - Body: response.Body, - IsDraft: response.IsDraft, - ModifiedAt: response.ModifiedAt, - QuestionnaireId: response.QuestionnaireId, - Respondent: &userID, - ResponseId: response.ResponseId, - SubmittedAt: response.SubmittedAt, - IsAnonymous: response.IsAnonymous, + responseGroups, pageMax, err := r.IRespondent.GetMyResponseGroups(ctx.Request().Context(), userID, questionnaireIDs, params.IsDraft, pageNum) + if err != nil { + if errors.Is(err, model.ErrTooLargePageNum) { + ctx.Logger().Infof("invalid myResponses params: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusBadRequest, err) } - responseLists[responseDetail.QuestionnaireID] = append(responseLists[responseDetail.QuestionnaireID], tmp) - responseQuestionnaireIDs = append(responseQuestionnaireIDs, responseDetail.QuestionnaireID) + ctx.Logger().Errorf("failed to get my response groups: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire responses: %w", err)) } + res.PageMax = pageMax - questionnaireIDExists := make(map[int]bool, len(responseQuestionnaireIDs)) - for _, questionnaireID := range responseQuestionnaireIDs { - if questionnaireIDExists[questionnaireID] { - continue + for _, responseGroup := range responseGroups { + var responseDueDateTime *time.Time + if responseGroup.QuestionnaireInfo.ResponseDueDateTime.Valid { + dueDateTime := responseGroup.QuestionnaireInfo.ResponseDueDateTime.Time + responseDueDateTime = &dueDateTime } - questionnaireIDExists[questionnaireID] = true - questionnaire, _, _, _, _, _, _, _, err := r.IQuestionnaire.GetQuestionnaireInfo(ctx.Request().Context(), questionnaireID) - if err != nil { - ctx.Logger().Errorf("failed to get questionnaire info: %+v", err) - return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire info: %w", err)) + groupItem := openapi.ResponseWithQuestionnaireInfoItem{ + QuestionnaireInfo: openapi.QuestionnaireInfo{ + CreatedAt: responseGroup.QuestionnaireInfo.CreatedAt, + IsTargetingMe: responseGroup.QuestionnaireInfo.IsTargetingMe, + ModifiedAt: responseGroup.QuestionnaireInfo.ModifiedAt, + ResponseDueDateTime: responseDueDateTime, + Title: responseGroup.QuestionnaireInfo.Title, + }, + Responses: []openapi.Response{}, } - isTargetingMe, err := r.ITarget.IsTargetingMe(ctx.Request().Context(), questionnaireID, userID) - if err != nil { - ctx.Logger().Errorf("failed to get target info: %+v", err) - return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get target info: %w", err)) - } + for _, responseDetail := range responseGroup.Responses { + respondent := userID + response, err := respondentDetail2ResponseWithMetadata(ctx, responseDetail, &respondent, responseGroup.QuestionnaireInfo.IsAnonymous) + if err != nil { + ctx.Logger().Errorf("failed to convert respondent detail into response: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to convert respondent detail into response: %w", err)) + } - questionnaireInfo := openapi.QuestionnaireInfo{ - CreatedAt: questionnaire.CreatedAt, - IsTargetingMe: isTargetingMe, - ModifiedAt: questionnaire.ModifiedAt, - ResponseDueDateTime: &questionnaire.ResTimeLimit.Time, - Title: questionnaire.Title, + groupItem.Responses = append(groupItem.Responses, response) } - responses := responseLists[questionnaireID] - res = append(res, openapi.ResponseWithQuestionnaireInfoItem{ - QuestionnaireInfo: questionnaireInfo, - Responses: responses, - }) + res.ResponseGroups = append(res.ResponseGroups, groupItem) } return res, nil @@ -217,8 +196,8 @@ func (r *Response) EditResponse(ctx echo.Context, responseID openapi.ResponseIDI responseMetas, err := responseBody2ResponseMetas(req.Body, questions) if err != nil { - ctx.Logger().Errorf("failed to convert response body into response metas: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to convert response body into response metas: %w", err)) + ctx.Logger().Infof("invalid response body: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid response body: %w", err)) } // validationでチェック diff --git a/controller/response_test.go b/controller/response_test.go index cdbb497a..4cb3aee1 100644 --- a/controller/response_test.go +++ b/controller/response_test.go @@ -148,7 +148,7 @@ func TestGetMyResponses(t *testing.T) { rec = httptest.NewRecorder() req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) ctx = e.NewContext(req, rec) - _, err = q.PostQuestionnaireResponse(ctx, questionnaireDetail.QuestionnaireId, newResponse, userOne) + response0, err := q.PostQuestionnaireResponse(ctx, questionnaireDetail.QuestionnaireId, newResponse, userOne) require.NoError(t, err) newResponse = sampleResponse @@ -159,7 +159,7 @@ func TestGetMyResponses(t *testing.T) { rec = httptest.NewRecorder() req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) ctx = e.NewContext(req, rec) - _, err = q.PostQuestionnaireResponse(ctx, questionnaireDetail.QuestionnaireId, newResponse, userOne) + response1, err := q.PostQuestionnaireResponse(ctx, questionnaireDetail.QuestionnaireId, newResponse, userOne) require.NoError(t, err) newResponse = sampleResponse @@ -171,7 +171,7 @@ func TestGetMyResponses(t *testing.T) { rec = httptest.NewRecorder() req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) ctx = e.NewContext(req, rec) - _, err = q.PostQuestionnaireResponse(ctx, questionnaireDetail.QuestionnaireId, newResponse, userOne) + responseDraft, err := q.PostQuestionnaireResponse(ctx, questionnaireDetail.QuestionnaireId, newResponse, userOne) require.NoError(t, err) newResponse = sampleResponse @@ -196,6 +196,31 @@ func TestGetMyResponses(t *testing.T) { response4, err := q.PostQuestionnaireResponse(ctx, questionnaireDetail.QuestionnaireId, newResponse, "myResponsesSpecialUser") require.NoError(t, err) + questionnaireAnonymous := sampleQuestionnaire + questionnaireAnonymous.IsAnonymous = true + e = echo.New() + body, err = json.Marshal(questionnaireAnonymous) + require.NoError(t, err) + req = httptest.NewRequest(http.MethodPost, "/questionnaires", bytes.NewReader(body)) + rec = httptest.NewRecorder() + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + ctx = e.NewContext(req, rec) + questionnaireAnonymousDetail, err := q.PostQuestionnaire(ctx, questionnaireAnonymous) + require.NoError(t, err) + + AddQuestionID2SampleResponse(questionnaireAnonymousDetail.QuestionnaireId) + + newResponse = sampleResponse + e = echo.New() + body, err = json.Marshal(newResponse) + require.NoError(t, err) + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/questionnaires/%d/responses", questionnaireAnonymousDetail.QuestionnaireId), bytes.NewReader(body)) + rec = httptest.NewRecorder() + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + ctx = e.NewContext(req, rec) + responseAnonymous, err := q.PostQuestionnaireResponse(ctx, questionnaireAnonymousDetail.QuestionnaireId, newResponse, userOne) + require.NoError(t, err) + AddQuestionID2SampleResponseMutex.Unlock() type args struct { @@ -203,9 +228,11 @@ func TestGetMyResponses(t *testing.T) { params openapi.GetMyResponsesParams } type expect struct { - isErr bool - err error - responseIDList *[]int + isErr bool + err error + pageMax *int + responseIDList *[]int + nilRespondentResponseID *[]int } type test struct { description string @@ -213,114 +240,120 @@ func TestGetMyResponses(t *testing.T) { expect } - sortInvalid := (openapi.ResponseSortInQuery)("abcde") - sortTraqID := (openapi.ResponseSortInQuery)("traqid") - sortTraqIDDesc := (openapi.ResponseSortInQuery)("-traqid") - sortSubmittedAt := (openapi.ResponseSortInQuery)("submitted_at") - sortSubmittedAtDesc := (openapi.ResponseSortInQuery)("-submitted_at") - sortModifiedAt := (openapi.ResponseSortInQuery)("modified_at") - sortModifiedAtDesc := (openapi.ResponseSortInQuery)("-modified_at") + pageZero := 0 + largePageNum := 100000000 + pageOne := 1 + constTrue := true + constFalse := false + questionnaireIDs := []int{questionnaireDetail.QuestionnaireId, questionnaireAnonymousDetail.QuestionnaireId} testCases := []test{ { description: "valid", - args: args{ - userID: userOne, - params: openapi.GetMyResponsesParams{}, - }, - }, - { - description: "invalid param sort", args: args{ userID: userOne, params: openapi.GetMyResponsesParams{ - Sort: &sortInvalid, + QuestionnaireIDs: &questionnaireIDs, }, }, expect: expect{ - isErr: true, + pageMax: &pageOne, + nilRespondentResponseID: &[]int{responseAnonymous.ResponseId}, }, }, { - description: "sort submitted_at", + description: "page less than one treated as first page", args: args{ userID: userOne, params: openapi.GetMyResponsesParams{ - Sort: &sortSubmittedAt, + Page: &pageZero, + QuestionnaireIDs: &questionnaireIDs, }, }, - }, - { - description: "sort -submitted_at", - args: args{ - userID: userOne, - params: openapi.GetMyResponsesParams{ - Sort: &sortSubmittedAtDesc, - }, + expect: expect{ + pageMax: &pageOne, }, }, { - description: "sort traqid", + description: "too large page num", args: args{ userID: userOne, params: openapi.GetMyResponsesParams{ - Sort: &sortTraqID, + Page: &largePageNum, + QuestionnaireIDs: &questionnaireIDs, }, }, - }, - { - description: "sort -traqid", - args: args{ - userID: userOne, - params: openapi.GetMyResponsesParams{ - Sort: &sortTraqIDDesc, - }, + expect: expect{ + isErr: true, }, }, { - description: "sort modified_at", + description: "draft only", args: args{ userID: userOne, params: openapi.GetMyResponsesParams{ - Sort: &sortModifiedAt, + IsDraft: &constTrue, + QuestionnaireIDs: &questionnaireIDs, }, }, + expect: expect{ + pageMax: &pageOne, + responseIDList: &[]int{responseDraft.ResponseId}, + nilRespondentResponseID: &[]int{}, + }, }, { - description: "sort -modified_at", + description: "non draft only", args: args{ userID: userOne, params: openapi.GetMyResponsesParams{ - Sort: &sortModifiedAtDesc, + IsDraft: &constFalse, + QuestionnaireIDs: &questionnaireIDs, }, }, + expect: expect{ + pageMax: &pageOne, + responseIDList: &[]int{response0.ResponseId, response1.ResponseId, responseAnonymous.ResponseId}, + nilRespondentResponseID: &[]int{responseAnonymous.ResponseId}, + }, }, { description: "special user", args: args{ userID: "myResponsesSpecialUser", - params: openapi.GetMyResponsesParams{}, + params: openapi.GetMyResponsesParams{ + QuestionnaireIDs: &questionnaireIDs, + }, }, expect: expect{ - responseIDList: &[]int{response4.ResponseId}, + pageMax: &pageOne, + responseIDList: &[]int{response4.ResponseId}, + nilRespondentResponseID: &[]int{}, }, }, { description: "user with no record", args: args{ userID: "myResponsesNoRecord", - params: openapi.GetMyResponsesParams{}, + params: openapi.GetMyResponsesParams{ + QuestionnaireIDs: &questionnaireIDs, + }, }, expect: expect{ - responseIDList: &[]int{}, + pageMax: &pageZero, + responseIDList: &[]int{}, + nilRespondentResponseID: &[]int{}, }, }, } for _, testCase := range testCases { params := url.Values{} - if testCase.args.params.Sort != nil { - params.Add("sort", string(*testCase.args.params.Sort)) + if testCase.args.params.Page != nil { + params.Add("page", fmt.Sprint(*testCase.args.params.Page)) + } + if testCase.args.params.IsDraft != nil { + params.Add("isDraft", fmt.Sprint(*testCase.args.params.IsDraft)) } e = echo.New() req = httptest.NewRequest(http.MethodGet, "/responses/myResponses"+params.Encode(), nil) @@ -341,64 +374,13 @@ func TestGetMyResponses(t *testing.T) { continue } - for _, responseList := range responseLists { - if testCase.args.params.Sort != nil { - switch *testCase.args.params.Sort { - case "submitted_at": - var preCreatedAt time.Time - for _, response := range responseList.Responses { - if !preCreatedAt.IsZero() { - assertion.False(preCreatedAt.After(response.SubmittedAt), testCase.description, "submitted_at") - } - preCreatedAt = response.SubmittedAt - } - case "-submitted_at": - var preCreatedAt time.Time - for _, response := range responseList.Responses { - if !preCreatedAt.IsZero() { - assertion.False(preCreatedAt.Before(response.SubmittedAt), testCase.description, "-submitted_at") - } - preCreatedAt = response.SubmittedAt - } - case "traqid": - var preTraqID string - for _, response := range responseList.Responses { - if preTraqID != "" { - assertion.False(preTraqID > *response.Respondent, testCase.description, "traqid") - } - preTraqID = *response.Respondent - } - case "-traqid": - var preTraqID string - for _, response := range responseList.Responses { - if preTraqID != "" { - assertion.False(preTraqID < *response.Respondent, testCase.description, "-traqid") - } - preTraqID = *response.Respondent - } - case "modified_at": - var preModifiedAt time.Time - for _, response := range responseList.Responses { - if !preModifiedAt.IsZero() { - assertion.False(preModifiedAt.After(response.ModifiedAt), testCase.description, "modified_at") - } - preModifiedAt = response.ModifiedAt - } - case "-modified_at": - var preModifiedAt time.Time - for _, response := range responseList.Responses { - if !preModifiedAt.IsZero() { - assertion.False(preModifiedAt.Before(response.ModifiedAt), testCase.description, "-modified_at") - } - preModifiedAt = response.ModifiedAt - } - } - } + if testCase.expect.pageMax != nil { + assertion.Equal(*testCase.expect.pageMax, responseLists.PageMax, testCase.description, "pageMax") } if testCase.expect.responseIDList != nil { responseIDList := []int{} - for _, responseList := range responseLists { + for _, responseList := range responseLists.ResponseGroups { for _, response := range responseList.Responses { responseIDList = append(responseIDList, response.ResponseId) } @@ -410,9 +392,29 @@ func TestGetMyResponses(t *testing.T) { assertion.Equal(*testCase.expect.responseIDList, responseIDList, testCase.description, "responseIDList") } - for _, responseList := range responseLists { + if testCase.expect.nilRespondentResponseID != nil { + nilRespondentResponseID := []int{} + for _, responseList := range responseLists.ResponseGroups { + for _, response := range responseList.Responses { + if response.Respondent == nil { + nilRespondentResponseID = append(nilRespondentResponseID, response.ResponseId) + } + } + } + sort.Slice(*testCase.expect.nilRespondentResponseID, func(i, j int) bool { + return (*testCase.expect.nilRespondentResponseID)[i] < (*testCase.expect.nilRespondentResponseID)[j] + }) + sort.Slice(nilRespondentResponseID, func(i, j int) bool { return nilRespondentResponseID[i] < nilRespondentResponseID[j] }) + assertion.Equal(*testCase.expect.nilRespondentResponseID, nilRespondentResponseID, testCase.description, "nilRespondentResponseID") + } + + for _, responseList := range responseLists.ResponseGroups { for _, response := range responseList.Responses { - assertion.Equal(testCase.args.userID, *response.Respondent, testCase.description, "response with no respondent") + if response.Respondent == nil { + assertion.True(response.IsAnonymous, testCase.description, "response with nil respondent should be anonymous") + continue + } + assertion.Equal(testCase.args.userID, *response.Respondent, testCase.description, "response respondent should match userID") } } } diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index d9f134a8..651327b1 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -298,18 +298,20 @@ paths: # TODO 変数の命名を確認する operationId: getMyResponses tags: - response - description: 自分のすべての回答のリストを取得します。 + description: 自分のすべての回答のリストを、アンケートごとにまとめてページ単位で取得します。 parameters: - - $ref: "#/components/parameters/responseSortInQuery" + - $ref: "#/components/parameters/pageInQuery" - $ref: "#/components/parameters/questionnaireIDsInQuery" - $ref: "#/components/parameters/isDraftInQuery" responses: "200": - description: 正常に取得できました。回答の配列を返します。 + description: 正常に取得できました。ページ情報付きの回答一覧を返します。 content: application/json: schema: $ref: "#/components/schemas/ResponsesWithQuestionnaireInfo" + "400": + description: 与えられた情報の形式が異なります "500": description: 自分の回答のリストを取得できませんでした /traq/users: @@ -1010,9 +1012,20 @@ components: items: $ref: "#/components/schemas/Response" ResponsesWithQuestionnaireInfo: - type: array - items: - $ref: "#/components/schemas/ResponseWithQuestionnaireInfoItem" + type: object + properties: + page_max: + type: integer + example: 1 + description: | + 合計のページ数 + response_groups: + type: array + items: + $ref: "#/components/schemas/ResponseWithQuestionnaireInfoItem" + required: + - page_max + - response_groups QuestionnaireInfo: type: object allOf: diff --git a/handler/response.go b/handler/response.go index 5ee21aeb..29910fbb 100644 --- a/handler/response.go +++ b/handler/response.go @@ -19,7 +19,7 @@ func (h Handler) GetMyResponses(ctx echo.Context, params openapi.GetMyResponsesP res, err := h.Response.GetMyResponses(ctx, params, userID) if err != nil { ctx.Logger().Errorf("failed to get my responses: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get my responses: %w", err)) + return err } return ctx.JSON(200, res) } diff --git a/model/respondents.go b/model/respondents.go index ea700357..f60aac96 100644 --- a/model/respondents.go +++ b/model/respondents.go @@ -4,10 +4,26 @@ package model import ( "context" + "time" "gopkg.in/guregu/null.v4" ) +type MyResponseQuestionnaireInfo struct { + QuestionnaireID int + Title string + CreatedAt time.Time + ModifiedAt time.Time + ResponseDueDateTime null.Time + IsAnonymous bool + IsTargetingMe bool +} + +type MyResponseGroup struct { + QuestionnaireInfo MyResponseQuestionnaireInfo + Responses []RespondentDetail +} + // IRespondent RespondentのRepository type IRespondent interface { InsertRespondent(ctx context.Context, userID string, questionnaireID int, submittedAt null.Time) (int, error) @@ -18,6 +34,7 @@ type IRespondent interface { GetRespondentInfos(ctx context.Context, userID string, questionnaireIDs ...int) ([]RespondentInfo, error) GetRespondentDetail(ctx context.Context, responseID int) (RespondentDetail, error) GetRespondentDetails(ctx context.Context, questionnaireID int, sort string, onlyMyResponse bool, userID string, isDraft *bool) ([]RespondentDetail, error) + GetMyResponseGroups(ctx context.Context, userID string, questionnaireIDs []int, isDraft *bool, pageNum int) ([]MyResponseGroup, int, error) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs []int) ([]Respondents, error) GetMyResponseIDs(ctx context.Context, sort string, userID string, questionnaireIDs []int, isDraft *bool) ([]int, error) CheckRespondent(ctx context.Context, userID string, questionnaireID int) (bool, error) diff --git a/model/respondents_impl.go b/model/respondents_impl.go index 168b525e..fe048cb7 100755 --- a/model/respondents_impl.go +++ b/model/respondents_impl.go @@ -428,6 +428,208 @@ func (*Respondent) GetRespondentDetails(ctx context.Context, questionnaireID int return respondentDetails, nil } +type myResponseGroupRow struct { + QuestionnaireID int `gorm:"column:questionnaire_id"` + Title string `gorm:"column:title"` + CreatedAt time.Time `gorm:"column:created_at"` + ModifiedAt time.Time `gorm:"column:modified_at"` + ResTimeLimit null.Time `gorm:"column:res_time_limit"` + IsAnonymous bool `gorm:"column:is_anonymous"` + IsTargetingMe bool `gorm:"column:is_targeting_me"` + FirstResponseID int `gorm:"column:first_response_id"` +} + +func buildMyResponseBaseQuery(db *gorm.DB, userID string, questionnaireIDs []int, isDraft *bool) *gorm.DB { + query := db. + Table("respondents"). + Joins("INNER JOIN questionnaires ON respondents.questionnaire_id = questionnaires.id"). + Where("respondents.deleted_at IS NULL AND questionnaires.deleted_at IS NULL AND respondents.user_traqid = ?", userID) + + if questionnaireIDs != nil { + query = query.Where("respondents.questionnaire_id IN (?)", questionnaireIDs) + } + + if isDraft != nil { + if *isDraft { + query = query.Where("respondents.submitted_at IS NULL") + } else { + query = query.Where("respondents.submitted_at IS NOT NULL") + } + } + + return query +} + +// GetMyResponseGroups 自分の回答をアンケートごとにまとめて取得 +func (*Respondent) GetMyResponseGroups(ctx context.Context, userID string, questionnaireIDs []int, isDraft *bool, pageNum int) ([]MyResponseGroup, int, error) { + db, err := getTx(ctx) + if err != nil { + return nil, 0, fmt.Errorf("failed to get transaction: %w", err) + } + + baseQuery := buildMyResponseBaseQuery(db, userID, questionnaireIDs, isDraft) + + var count int64 + err = baseQuery. + Session(&gorm.Session{}). + Distinct("respondents.questionnaire_id"). + Count(&count).Error + if err != nil { + return nil, 0, fmt.Errorf("failed to count my response questionnaires: %w", err) + } + + if count == 0 { + return []MyResponseGroup{}, 0, nil + } + + pageMax := (int(count) + 19) / 20 + if pageNum > pageMax { + return nil, 0, ErrTooLargePageNum + } + + groupRows := []myResponseGroupRow{} + groupQuery := buildMyResponseBaseQuery(db, userID, questionnaireIDs, isDraft). + Select( + "respondents.questionnaire_id, questionnaires.title, questionnaires.created_at, questionnaires.modified_at, questionnaires.res_time_limit, questionnaires.is_anonymous, "+ + "EXISTS(SELECT 1 FROM targets WHERE targets.questionnaire_id = questionnaires.id AND targets.user_traqid = ?) AS is_targeting_me, "+ + "MIN(respondents.response_id) AS first_response_id", + userID, + ). + Group("respondents.questionnaire_id, questionnaires.id, questionnaires.title, questionnaires.created_at, questionnaires.modified_at, questionnaires.res_time_limit, questionnaires.is_anonymous"). + Order("first_response_id") + + err = groupQuery. + Limit(20). + Offset((pageNum - 1) * 20). + Find(&groupRows).Error + if err != nil { + return nil, 0, fmt.Errorf("failed to get my response groups: %w", err) + } + + if len(groupRows) == 0 { + return []MyResponseGroup{}, pageMax, nil + } + + groups := make([]MyResponseGroup, 0, len(groupRows)) + groupIndexByQuestionnaireID := make(map[int]int, len(groupRows)) + pageQuestionnaireIDs := make([]int, 0, len(groupRows)) + for i, row := range groupRows { + groups = append(groups, MyResponseGroup{ + QuestionnaireInfo: MyResponseQuestionnaireInfo{ + QuestionnaireID: row.QuestionnaireID, + Title: row.Title, + CreatedAt: row.CreatedAt, + ModifiedAt: row.ModifiedAt, + ResponseDueDateTime: row.ResTimeLimit, + IsAnonymous: row.IsAnonymous, + IsTargetingMe: row.IsTargetingMe, + }, + Responses: []RespondentDetail{}, + }) + groupIndexByQuestionnaireID[row.QuestionnaireID] = i + pageQuestionnaireIDs = append(pageQuestionnaireIDs, row.QuestionnaireID) + } + + respondents := []Respondents{} + respondentQuery := db. + Session(&gorm.Session{}). + Where("respondents.deleted_at IS NULL AND respondents.user_traqid = ? AND respondents.questionnaire_id IN (?)", userID, pageQuestionnaireIDs). + Select("ResponseID", "QuestionnaireID", "UserTraqid", "ModifiedAt", "SubmittedAt") + if isDraft != nil { + if *isDraft { + respondentQuery = respondentQuery.Where("submitted_at IS NULL") + } else { + respondentQuery = respondentQuery.Where("submitted_at IS NOT NULL") + } + } + respondentQuery = respondentQuery.Order("response_id") + + err = respondentQuery.Find(&respondents).Error + if err != nil { + return nil, 0, fmt.Errorf("failed to get respondents for my response groups: %w", err) + } + + if len(respondents) == 0 { + return groups, pageMax, nil + } + + responseIDs := make([]int, 0, len(respondents)) + respondentDetailMap := make(map[int]*RespondentDetail, len(respondents)) + responseIDsByQuestionnaireID := make(map[int][]int, len(groupRows)) + for _, respondent := range respondents { + groupIdx := groupIndexByQuestionnaireID[respondent.QuestionnaireID] + groups[groupIdx].Responses = append(groups[groupIdx].Responses, RespondentDetail{ + ResponseID: respondent.ResponseID, + TraqID: respondent.UserTraqid, + QuestionnaireID: respondent.QuestionnaireID, + ModifiedAt: respondent.ModifiedAt, + SubmittedAt: respondent.SubmittedAt, + Responses: []ResponseBody{}, + }) + lastIdx := len(groups[groupIdx].Responses) - 1 + respondentDetailMap[respondent.ResponseID] = &groups[groupIdx].Responses[lastIdx] + responseIDs = append(responseIDs, respondent.ResponseID) + responseIDsByQuestionnaireID[respondent.QuestionnaireID] = append(responseIDsByQuestionnaireID[respondent.QuestionnaireID], respondent.ResponseID) + } + + questions := []Questions{} + err = db. + Preload("Responses", func(db *gorm.DB) *gorm.DB { + return db. + Select("ResponseID", "QuestionID", "Body"). + Where("response_id IN (?)", responseIDs) + }). + Where("questionnaire_id IN (?)", pageQuestionnaireIDs). + Order("questionnaire_id"). + Order("question_num"). + Select("ID", "QuestionnaireID", "QuestionNum", "Type"). + Find(&questions).Error + if err != nil { + return nil, 0, fmt.Errorf("failed to get questions for my response groups: %w", err) + } + + for _, question := range questions { + responseBodyMap := make(map[int][]string) + for _, response := range question.Responses { + if response.Body.Valid { + responseBodyMap[response.ResponseID] = append(responseBodyMap[response.ResponseID], response.Body.String) + } + } + + for _, responseID := range responseIDsByQuestionnaireID[question.QuestionnaireID] { + respondentDetail := respondentDetailMap[responseID] + if respondentDetail == nil { + continue + } + + responseBodies := responseBodyMap[responseID] + responseBody := ResponseBody{ + QuestionID: question.ID, + QuestionType: question.Type, + } + + switch question.Type { + case "MultipleChoice", "Checkbox", "Dropdown": + if responseBodies == nil { + responseBody.OptionResponse = []string{} + } else { + responseBody.OptionResponse = responseBodies + } + default: + if len(responseBodies) == 0 { + responseBody.Body = null.NewString("", false) + } else { + responseBody.Body = null.NewString(responseBodies[0], true) + } + } + + respondentDetail.Responses = append(respondentDetail.Responses, responseBody) + } + } + + return groups, pageMax, nil +} + // GetRespondentsUserIDs 回答者のユーザーID取得 func (*Respondent) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs []int) ([]Respondents, error) { db, err := getTx(ctx) diff --git a/openapi/server.go b/openapi/server.go index 5c98442c..4ddadc0c 100644 --- a/openapi/server.go +++ b/openapi/server.go @@ -291,11 +291,11 @@ func (w *ServerInterfaceWrapper) GetMyResponses(ctx echo.Context) error { // Parameter object where we will unmarshal all parameters from the context var params GetMyResponsesParams - // ------------- Optional query parameter "sort" ------------- + // ------------- Optional query parameter "page" ------------- - err = runtime.BindQueryParameter("form", true, false, "sort", ctx.QueryParams(), ¶ms.Sort) + err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter sort: %s", err)) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) } // ------------- Optional query parameter "questionnaireIDs" ------------- diff --git a/openapi/spec.go b/openapi/spec.go index 84bf90d8..5d69b2d8 100644 --- a/openapi/spec.go +++ b/openapi/spec.go @@ -18,92 +18,92 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "Sj7DrJ8cmfz/AAAA//9u1cnNkX8AAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi/types.go b/openapi/types.go index 8062edc0..444b6a3e 100644 --- a/openapi/types.go +++ b/openapi/types.go @@ -645,7 +645,11 @@ type ResponseWithQuestionnaireInfoItem struct { type Responses = []Response // ResponsesWithQuestionnaireInfo defines model for ResponsesWithQuestionnaireInfo. -type ResponsesWithQuestionnaireInfo = []ResponseWithQuestionnaireInfoItem +type ResponsesWithQuestionnaireInfo struct { + // PageMax 合計のページ数 + PageMax int `json:"page_max"` + ResponseGroups []ResponseWithQuestionnaireInfoItem `json:"response_groups"` +} // SortType question、questionnaire用のソートの種類 type SortType string @@ -804,8 +808,8 @@ type GetQuestionnaireResponsesParams struct { // GetMyResponsesParams defines parameters for GetMyResponses. type GetMyResponsesParams struct { - // Sort 並び順 (作成日時が新しい "submitted_at", 作成日時が古い "-submitted_at", TraqIDの昇順 "traqid", TraqIDの降順 "-traqid", 更新日時が新しい "modified_at", 更新日時が古い "-modified_at" ) - Sort *ResponseSortInQuery `form:"sort,omitempty" json:"sort,omitempty"` + // Page 何ページ目か (未定義の場合は1ページ目) + Page *PageInQuery `form:"page,omitempty" json:"page,omitempty"` // QuestionnaireIDs 取得したい情報のアンケートをフィルタリングするためのパラメータ。複数指定可能。 QuestionnaireIDs *QuestionnaireIDsInQuery `form:"questionnaireIDs,omitempty" json:"questionnaireIDs,omitempty"`