diff --git a/controller/questionnaire.go b/controller/questionnaire.go index 2db3b479..f1d4c2bd 100644 --- a/controller/questionnaire.go +++ b/controller/questionnaire.go @@ -15,15 +15,6 @@ import ( "gopkg.in/guregu/null.v4" ) -// Response Responseの構造体 -type Response struct { - model.IQuestionnaire - model.IValidation - model.IScaleLabel - model.IRespondent - model.IResponse -} - // Questionnaire Questionnaireの構造体 type Questionnaire struct { model.IQuestionnaire @@ -424,3 +415,35 @@ https://anke-to.trap.jp/responses/new/%d`, questionnaireID, ) } + +func (q Questionnaire) GetQuestionnaireResult(ctx echo.Context, questionnaireID int, userID string) (openapi.Result, error) { + res := openapi.Result{} + + params := openapi.GetQuestionnaireResponsesParams{} + responses, err := q.GetQuestionnaireResponses(ctx, questionnaireID, params, userID) + if err != nil { + ctx.Logger().Errorf("failed to get questionnaire responses: %+v", err) + return openapi.Result{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire responses: %w", err)) + } + + for _, response := range responses { + tmp := struct { + Body []openapi.ResponseBody `json:"body"` + IsDraft bool `json:"is_draft"` + ModifiedAt time.Time `json:"modified_at"` + QuestionnaireId int `json:"questionnaire_id"` + ResponseId int `json:"response_id"` + SubmittedAt time.Time `json:"submitted_at"` + }{ + Body: response.Body, + IsDraft: response.IsDraft, + ModifiedAt: response.ModifiedAt, + QuestionnaireId: response.QuestionnaireId, + ResponseId: response.ResponseId, + SubmittedAt: response.SubmittedAt, + } + res = append(res, tmp) + } + + return res, nil +} diff --git a/controller/response.go b/controller/response.go new file mode 100644 index 00000000..a7bd6927 --- /dev/null +++ b/controller/response.go @@ -0,0 +1,155 @@ +package controller + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/labstack/echo/v4" + "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/openapi" +) + +// Response Responseの構造体 +type Response struct { + model.IQuestionnaire + model.IRespondent + model.IResponse + model.ITarget +} + +func NewResponse() *Response { + return &Response{} +} + +func (r Response) GetMyResponses(ctx echo.Context, params openapi.GetMyResponsesParams, userID string) (openapi.ResponsesWithQuestionnaireInfo, error) { + res := openapi.ResponsesWithQuestionnaireInfo{} + + sort := string(*params.Sort) + responsesID := []int{} + responsesID, err := r.IRespondent.GetMyResponseIDs(ctx.Request().Context(), sort, userID) + 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)) + } + + 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)) + } + + questionnaire, _, _, _, _, _, err := r.IQuestionnaire.GetQuestionnaireInfo(ctx.Request().Context(), responseDetail.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)) + } + + isTargetingMe, err := r.ITarget.IsTargetingMe(ctx.Request().Context(), responseDetail.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)) + } + + questionnaireInfo := struct { + CreatedAt time.Time `json:"created_at"` + IsTargetingMe bool `json:"is_targeting_me"` + ModifiedAt time.Time `json:"modified_at"` + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` + Title string `json:"title"` + }{ + CreatedAt: questionnaire.CreatedAt, + IsTargetingMe: isTargetingMe, + ModifiedAt: questionnaire.ModifiedAt, + ResponseDueDateTime: &questionnaire.ResTimeLimit.Time, + Title: questionnaire.Title, + } + + 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 := struct { + Body []openapi.ResponseBody `json:"body"` + IsDraft bool `json:"is_draft"` + ModifiedAt time.Time `json:"modified_at"` + QuestionnaireId int `json:"questionnaire_id"` + QuestionnaireInfo *struct { + CreatedAt time.Time `json:"created_at"` + IsTargetingMe bool `json:"is_targeting_me"` + ModifiedAt time.Time `json:"modified_at"` + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` + Title string `json:"title"` + } `json:"questionnaire_info,omitempty"` + Respondent openapi.TraqId `json:"respondent"` + ResponseId int `json:"response_id"` + SubmittedAt time.Time `json:"submitted_at"` + }{ + Body: response.Body, + IsDraft: response.IsDraft, + ModifiedAt: response.ModifiedAt, + QuestionnaireId: response.QuestionnaireId, + QuestionnaireInfo: &questionnaireInfo, + Respondent: userID, + ResponseId: response.ResponseId, + SubmittedAt: response.SubmittedAt, + } + res = append(res, tmp) + } + + return res, nil +} + +func (r Response) GetResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) (openapi.Response, error) { + responseDetail, err := r.IRespondent.GetRespondentDetail(ctx.Request().Context(), responseID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + ctx.Logger().Errorf("failed to find response by response ID: %+v", err) + return openapi.Response{}, echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("failed to find response by response ID: %w", err)) + } + ctx.Logger().Errorf("failed to get respondent detail: %+v", err) + return openapi.Response{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get respondent detail: %w", err)) + } + + res, err := respondentDetail2Response(ctx, responseDetail) + if err != nil { + ctx.Logger().Errorf("failed to convert respondent detail into response: %+v", err) + return openapi.Response{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to convert respondent detail into response: %w", err)) + } + + return res, nil +} + +func (r Response) DeleteResponse(ctx echo.Context, responseID openapi.ResponseIDInPath, userID string) error { + limit, err := r.IQuestionnaire.GetQuestionnaireLimitByResponseID(ctx.Request().Context(), responseID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + ctx.Logger().Errorf("failed to find response by response ID: %+v", err) + return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("failed to find response by response ID: %w", err)) + } + ctx.Logger().Errorf("failed to get questionnaire limit by response ID: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire limit by response ID: %w", err)) + } + if limit.Valid && limit.Time.Before(time.Now()) { + ctx.Logger().Errorf("unable delete the expired response") + return echo.NewHTTPError(http.StatusMethodNotAllowed, fmt.Errorf("unable delete the expired response")) + } + + err = r.IRespondent.DeleteRespondent(ctx.Request().Context(), responseID) + if err != nil { + ctx.Logger().Errorf("failed to delete respondent: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to delete respondent: %w", err)) + } + + err = r.IResponse.DeleteResponse(ctx.Request().Context(), responseID) + if err != nil { + ctx.Logger().Errorf("failed to delete response: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to delete response: %w", err)) + } + + return nil +} diff --git a/handler/questionnaire.go b/handler/questionnaire.go index 0490f43b..c6fe5dc1 100644 --- a/handler/questionnaire.go +++ b/handler/questionnaire.go @@ -183,6 +183,18 @@ func (h Handler) PostQuestionnaireResponse(ctx echo.Context, questionnaireID ope // (GET /questionnaires/{questionnaireID}/result) func (h Handler) GetQuestionnaireResult(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { res := openapi.Result{} + userID, err := getUserID(ctx) + if err != nil { + ctx.Logger().Errorf("failed to get userID: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) + } + + q := controller.NewQuestionnaire() + res, err = q.GetQuestionnaireResult(ctx, questionnaireID, userID) + if err != nil { + ctx.Logger().Errorf("failed to get questionnaire result: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire result: %w", err)) + } return ctx.JSON(200, res) } diff --git a/handler/response.go b/handler/response.go index 6212214f..1f47bcd2 100644 --- a/handler/response.go +++ b/handler/response.go @@ -1,19 +1,47 @@ package handler import ( + "fmt" + "net/http" + "github.com/labstack/echo/v4" + "github.com/traPtitech/anke-to/controller" "github.com/traPtitech/anke-to/openapi" ) // (GET /responses/myResponses) func (h Handler) GetMyResponses(ctx echo.Context, params openapi.GetMyResponsesParams) error { - res := []openapi.ResponsesWithQuestionnaireInfo{} + res := openapi.ResponsesWithQuestionnaireInfo{} + userID, err := getUserID(ctx) + if err != nil { + ctx.Logger().Errorf("failed to get userID: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) + } + r := controller.NewResponse() + res, err = r.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 ctx.JSON(200, res) } // (DELETE /responses/{responseID}) func (h Handler) DeleteResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) error { + userID, err := getUserID(ctx) + if err != nil { + ctx.Logger().Errorf("failed to get userID: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) + } + + r := controller.NewResponse() + err = r.DeleteResponse(ctx, responseID, userID) + if err != nil { + ctx.Logger().Errorf("failed to delete response: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to delete response: %w", err)) + } + return ctx.NoContent(200) } @@ -21,6 +49,12 @@ func (h Handler) DeleteResponse(ctx echo.Context, responseID openapi.ResponseIDI func (h Handler) GetResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) error { res := openapi.Response{} + r := controller.NewResponse() + res, err := r.GetResponse(ctx, responseID) + if err != nil { + ctx.Logger().Errorf("failed to get response: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get response: %w", err)) + } return ctx.JSON(200, res) } diff --git a/model/respondents.go b/model/respondents.go index 56813245..e5cff356 100644 --- a/model/respondents.go +++ b/model/respondents.go @@ -18,5 +18,6 @@ type IRespondent interface { GetRespondentDetail(ctx context.Context, responseID int) (RespondentDetail, error) GetRespondentDetails(ctx context.Context, questionnaireID int, sort string, onlyMyResponse bool, userID string) ([]RespondentDetail, error) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs []int) ([]Respondents, error) + GetMyResponseIDs(ctx context.Context, sort string, userID string) ([]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 bcf55d27..2cbf4aaf 100755 --- a/model/respondents_impl.go +++ b/model/respondents_impl.go @@ -386,6 +386,26 @@ func (*Respondent) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs [ return respondents, nil } +// GetMyResponses 自分のすべての回答を取得 +func (*Respondent) GetMyResponseIDs(ctx context.Context, userID string) ([]int, error) { + db, err := getTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get transaction: %w", err) + } + + responsesID := []int{} + err = db. + Model(&Respondents{}). + Where("user_traqid = ?", userID). + Select("response_id"). + Find(&responsesID).Error + if err != nil { + return nil, fmt.Errorf("failed to get responsesID: %w", err) + } + + return responsesID, nil +} + // CheckRespondent 回答者かどうかの確認 func (*Respondent) CheckRespondent(ctx context.Context, userID string, questionnaireID int) (bool, error) { db, err := getTx(ctx) diff --git a/model/respondents_test.go b/model/respondents_test.go index cafc7dd7..cb38c8e9 100644 --- a/model/respondents_test.go +++ b/model/respondents_test.go @@ -990,6 +990,101 @@ func TestGetRespondentsUserIDs(t *testing.T) { } } +func TestGetMyResponseIDs(t *testing.T) { + t.Parallel() + + assertion := assert.New(t) + ctx := context.Background() + + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + require.NoError(t, err) + + respondents := []Respondents{ + { + QuestionnaireID: questionnaireID, + UserTraqid: userOne, + SubmittedAt: null.NewTime(time.Now(), true), + }, + { + QuestionnaireID: questionnaireID, + UserTraqid: userTwo, + SubmittedAt: null.NewTime(time.Now(), true), + }, + { + QuestionnaireID: questionnaireID, + UserTraqid: userTwo, + SubmittedAt: null.NewTime(time.Now(), true), + }, + } + responseIDs := []int{} + for _, respondent := range respondents { + responseID, err := respondentImpl.InsertRespondent(ctx, respondent.UserTraqid, questionnaireID, respondent.SubmittedAt) + require.NoError(t, err) + responseIDs = append(responseIDs, responseID) + } + + type args struct { + userID string + } + type expect struct { + isErr bool + err error + responseIDs []int + } + type test struct { + description string + args + expect + } + + testCases := []test{ + { + description: "valid user with one resonse", + args: args{ + userID: userOne, + }, + expect: expect{ + responseIDs: []int{responseIDs[1]}, + }, + }, + { + description: "valid user with multiple responses", + args: args{ + userID: userTwo, + }, + expect: expect{ + responseIDs: []int{responseIDs[2], responseIDs[3]}, + }, + }, + { + description: "valid user with no response", + args: args{ + userID: userThree, + }, + expect: expect{ + responseIDs: []int{}, + }, + }, + } + + for _, testCase := range testCases { + MyResponseIDs, err := respondentImpl.GetMyResponseIDs(ctx, testCase.args.userID) + + if !testCase.expect.isErr { + assertion.NoError(err, testCase.description, "no error") + } else if testCase.expect.err != nil { + assertion.Equal(true, errors.Is(err, testCase.expect.err), testCase.description, "errorIs") + } else { + assertion.Error(err, testCase.description, "any error") + } + if err != nil { + continue + } + + assertion.Equal(testCase.expect.responseIDs, MyResponseIDs, testCase.description, "responseIDs") + } +} + func TestTestCheckRespondent(t *testing.T) { t.Parallel() diff --git a/model/targets.go b/model/targets.go index e6557b12..a1bb5506 100644 --- a/model/targets.go +++ b/model/targets.go @@ -9,4 +9,5 @@ type ITarget interface { InsertTargets(ctx context.Context, questionnaireID int, targets []string) error DeleteTargets(ctx context.Context, questionnaireID int) error GetTargets(ctx context.Context, questionnaireIDs []int) ([]Targets, error) + IsTargetingMe(ctx context.Context, quesionnairID int, userID string) (bool, error) } diff --git a/model/targets_impl.go b/model/targets_impl.go index c3d73dd8..6e046217 100644 --- a/model/targets_impl.go +++ b/model/targets_impl.go @@ -13,7 +13,7 @@ func NewTarget() *Target { return new(Target) } -//Targets targetsテーブルの構造体 +// Targets targetsテーブルの構造体 type Targets struct { QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` @@ -80,3 +80,24 @@ func (*Target) GetTargets(ctx context.Context, questionnaireIDs []int) ([]Target return targets, nil } + +func (*Target) IsTargetingMe(ctx context.Context, questionnairID int, userID string) (bool, error) { + db, err := getTx(ctx) + if err != nil { + return false, fmt.Errorf("failed to get transaction: %w", err) + } + + var count int64 + err = db. + Model(&Targets{}). + Where("questionnaire_id = ? AND user_traqid = ?", questionnairID, userID). + Count(&count).Error + if err != nil { + return false, fmt.Errorf("failed to get targets which are targeting me: %w", err) + } + + if count > 0 { + return true, nil + } + return false, nil +} diff --git a/model/targets_test.go b/model/targets_test.go index a00d7487..71404545 100644 --- a/model/targets_test.go +++ b/model/targets_test.go @@ -4,8 +4,11 @@ import ( "context" "errors" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/guregu/null.v4" "gorm.io/gorm" ) @@ -308,3 +311,68 @@ func TestGetTargets(t *testing.T) { }) } } + +func TestIsTargetingMe(t *testing.T) { + t.Parallel() + + assertion := assert.New(t) + ctx := context.Background() + + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + require.NoError(t, err) + + err = targetImpl.InsertTargets(ctx, questionnaireID, []string{userOne}) + require.NoError(t, err) + + type args struct { + userID string + } + type expect struct { + isErr bool + err error + isTargeted bool + } + type test struct { + description string + args + expect + } + + testCases := []test{ + { + description: "is targeted", + args: args{ + userID: userOne, + }, + expect: expect{ + isTargeted: true, + }, + }, + { + description: "not targeted", + args: args{ + userID: userTwo, + }, + expect: expect{ + isTargeted: false, + }, + }, + } + + for _, testCase := range testCases { + isTargeted, err := targetImpl.IsTargetingMe(ctx, questionnaireID, testCase.args.userID) + + if !testCase.expect.isErr { + assertion.NoError(err, testCase.description, "no error") + } else if testCase.expect.err != nil { + assertion.Equal(true, errors.Is(err, testCase.expect.err), testCase.description, "errorIs") + } else { + assertion.Error(err, testCase.description, "any error") + } + if err != nil { + continue + } + + assertion.Equal(testCase.expect.isTargeted, isTargeted, testCase.description, "isTargeted") + } +}