From 79e6e215710f9614724b00352feba9d5ec67aac0 Mon Sep 17 00:00:00 2001 From: kaitoyama Date: Tue, 19 Dec 2023 19:12:27 +0900 Subject: [PATCH 1/4] implement anonymous response --- model/current.go | 5 +- model/migration/v1.go | 105 +++++++++++++++++++++ model/questionnaires.go | 5 +- model/questionnaires_impl.go | 59 +++++++++--- model/questionnaires_test.go | 66 +++++++++++++- model/respondents.go | 1 + model/respondents_impl.go | 171 +++++++++++++++++++++++++++++++++++ model/respondents_test.go | 18 ++-- model/responses_test.go | 4 +- model/scale_labels_test.go | 10 +- model/validations_test.go | 8 +- router.go | 10 +- router/middleware.go | 39 +++++++- router/questionnaires.go | 70 +++++++++++--- router/results.go | 22 ----- 15 files changed, 509 insertions(+), 84 deletions(-) create mode 100644 model/migration/v1.go diff --git a/model/current.go b/model/current.go index 9b3fea3e..570ed8a5 100644 --- a/model/current.go +++ b/model/current.go @@ -2,11 +2,14 @@ package model import ( "github.com/go-gormigrate/gormigrate/v2" + "github.com/traPtitech/anke-to/model/migration" ) // Migrations is all db migrations func Migrations() []*gormigrate.Migration { - return []*gormigrate.Migration{} + return []*gormigrate.Migration{ + migration.V1(), // Questionnariesにis_anonymousカラムを追加 + } } func AllTables() []interface{} { diff --git a/model/migration/v1.go b/model/migration/v1.go new file mode 100644 index 00000000..7196ecd8 --- /dev/null +++ b/model/migration/v1.go @@ -0,0 +1,105 @@ +package migration + +import ( + "time" + + "github.com/go-gormigrate/gormigrate/v2" + "gopkg.in/guregu/null.v4" + "gorm.io/gorm" +) + +func V1() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "1", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate( + &v1Questionnaires{}, + ); err != nil { + return err + } + return nil + }, + } +} + +type v1Questionnaires struct { + ID int `json:"questionnaireID" gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + Title string `json:"title" gorm:"type:char(50);size:50;not null"` + Description string `json:"description" gorm:"type:text;not null"` + ResTimeLimit null.Time `json:"res_time_limit,omitempty" gorm:"type:TIMESTAMP NULL;default:NULL;"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"type:TIMESTAMP NULL;default:NULL;"` + ResSharedTo string `json:"res_shared_to" gorm:"type:char(30);size:30;not null;default:administrators"` + IsAnonymous bool `json:"is_anonymous" gorm:"type:boolean;not null;default:false"` + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` + ModifiedAt time.Time `json:"modified_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` + Administrators []v1Administrators `json:"-" gorm:"foreignKey:QuestionnaireID"` + Targets []v1Targets `json:"-" gorm:"foreignKey:QuestionnaireID"` + Questions []v1Questions `json:"-" gorm:"foreignKey:QuestionnaireID"` + Respondents []v1Respondents `json:"-" gorm:"foreignKey:QuestionnaireID"` +} + +type v1Administrators struct { + QuestionnaireID int `gorm:"type:int(11);not null;primaryKey"` + UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` +} + +type v1Targets struct { + QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` +} + +type v1Questions struct { + ID int `json:"id" gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + QuestionnaireID int `json:"questionnaireID" gorm:"type:int(11);not null"` + PageNum int `json:"page_num" gorm:"type:int(11);not null"` + QuestionNum int `json:"question_num" gorm:"type:int(11);not null"` + Type string `json:"type" gorm:"type:char(20);size:20;not null"` + Body string `json:"body" gorm:"type:text;default:NULL"` + IsRequired bool `json:"is_required" gorm:"type:tinyint(4);size:4;not null;default:0"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"type:TIMESTAMP NULL;default:NULL"` + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` + Options []v1Options `json:"-" gorm:"foreignKey:QuestionID"` + Responses []v1Responses `json:"-" gorm:"foreignKey:QuestionID"` + ScaleLabels []v1ScaleLabels `json:"-" gorm:"foreignKey:QuestionID"` + Validations []v1Validations `json:"-" gorm:"foreignKey:QuestionID"` +} + +type v1Respondents struct { + ResponseID int `json:"responseID" gorm:"column:response_id;type:int(11) AUTO_INCREMENT;not null;primaryKey"` + QuestionnaireID int `json:"questionnaireID" gorm:"type:int(11);not null"` + UserTraqid string `json:"user_traq_id,omitempty" gorm:"type:varchar(32);size:32;default:NULL"` + ModifiedAt time.Time `json:"modified_at,omitempty" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` + SubmittedAt null.Time `json:"submitted_at,omitempty" gorm:"type:TIMESTAMP NULL;default:NULL"` + DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"type:TIMESTAMP NULL;default:NULL"` + Responses []v1Responses `json:"-" gorm:"foreignKey:ResponseID;references:ResponseID"` +} + +type v1Options struct { + ID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + QuestionID int `gorm:"type:int(11);not null"` + OptionNum int `gorm:"type:int(11);not null"` + Body string `gorm:"type:text;default:NULL;"` +} + +type v1Responses struct { + ResponseID int `json:"-" gorm:"type:int(11);not null"` + QuestionID int `json:"-" gorm:"type:int(11);not null"` + Body null.String `json:"response" gorm:"type:text;default:NULL"` + ModifiedAt time.Time `json:"-" gorm:"type:timestamp;not null;dafault:CURRENT_TIMESTAMP"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"type:TIMESTAMP NULL;default:NULL"` +} + +type v1ScaleLabels struct { + QuestionID int `json:"questionID" gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + ScaleLabelRight string `json:"scale_label_right" gorm:"type:text;default:NULL;"` + ScaleLabelLeft string `json:"scale_label_left" gorm:"type:text;default:NULL;"` + ScaleMin int `json:"scale_min" gorm:"type:int(11);default:NULL;"` + ScaleMax int `json:"scale_max" gorm:"type:int(11);default:NULL;"` +} + +type v1Validations struct { + QuestionID int `json:"questionID" gorm:"type:int(11);not null;primaryKey"` + RegexPattern string `json:"regex_pattern" gorm:"type:text;default:NULL"` + MinBound string `json:"min_bound" gorm:"type:text;default:NULL"` + MaxBound string `json:"max_bound" gorm:"type:text;default:NULL"` +} diff --git a/model/questionnaires.go b/model/questionnaires.go index ec759d2e..d67be1ef 100644 --- a/model/questionnaires.go +++ b/model/questionnaires.go @@ -10,8 +10,8 @@ import ( // IQuestionnaire QuestionnaireのRepository type IQuestionnaire interface { - InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string) (int, error) - UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int) error + InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isAnonymous bool) (int, error) + UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isAnonymous bool, questionnaireID int) error DeleteQuestionnaire(ctx context.Context, questionnaireID int) error GetQuestionnaires(ctx context.Context, userID string, sort string, search string, pageNum int, nontargeted bool) ([]QuestionnaireInfo, int, error) GetAdminQuestionnaires(ctx context.Context, userID string) ([]Questionnaires, error) @@ -21,4 +21,5 @@ type IQuestionnaire interface { GetQuestionnaireLimitByResponseID(ctx context.Context, responseID int) (null.Time, error) GetResponseReadPrivilegeInfoByResponseID(ctx context.Context, userID string, responseID int) (*ResponseReadPrivilegeInfo, error) GetResponseReadPrivilegeInfoByQuestionnaireID(ctx context.Context, userID string, questionnaireID int) (*ResponseReadPrivilegeInfo, error) + GetResponseAnonymousByQuestionnaireID(ctx context.Context, questionnaireID int) (bool, error) } diff --git a/model/questionnaires_impl.go b/model/questionnaires_impl.go index 5aefbb78..4cc06ed9 100755 --- a/model/questionnaires_impl.go +++ b/model/questionnaires_impl.go @@ -19,7 +19,7 @@ func NewQuestionnaire() *Questionnaire { return new(Questionnaire) } -//Questionnaires questionnairesテーブルの構造体 +// Questionnaires questionnairesテーブルの構造体 type Questionnaires struct { ID int `json:"questionnaireID" gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` Title string `json:"title" gorm:"type:char(50);size:50;not null"` @@ -27,6 +27,7 @@ type Questionnaires struct { ResTimeLimit null.Time `json:"res_time_limit,omitempty" gorm:"type:TIMESTAMP NULL;default:NULL;"` DeletedAt gorm.DeletedAt `json:"-" gorm:"type:TIMESTAMP NULL;default:NULL;"` ResSharedTo string `json:"res_shared_to" gorm:"type:char(30);size:30;not null;default:administrators"` + IsAnonymous bool `json:"is_anonymous" gorm:"type:boolean;not null;default:false"` CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` ModifiedAt time.Time `json:"modified_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` Administrators []Administrators `json:"-" gorm:"foreignKey:QuestionnaireID"` @@ -44,20 +45,20 @@ func (questionnaire *Questionnaires) BeforeCreate(tx *gorm.DB) error { return nil } -//BeforeUpdate Update時に自動でmodified_atを現在時刻に +// BeforeUpdate Update時に自動でmodified_atを現在時刻に func (questionnaire *Questionnaires) BeforeUpdate(tx *gorm.DB) error { questionnaire.ModifiedAt = time.Now() return nil } -//QuestionnaireInfo Questionnaireにtargetかの情報追加 +// QuestionnaireInfo Questionnaireにtargetかの情報追加 type QuestionnaireInfo struct { Questionnaires IsTargeted bool `json:"is_targeted" gorm:"type:boolean"` } -//QuestionnaireDetail Questionnaireの詳細 +// QuestionnaireDetail Questionnaireの詳細 type QuestionnaireDetail struct { Targets []string Respondents []string @@ -65,7 +66,7 @@ type QuestionnaireDetail struct { Questionnaires } -//TargettedQuestionnaire targetになっているアンケートの情報 +// TargettedQuestionnaire targetになっているアンケートの情報 type TargettedQuestionnaire struct { Questionnaires RespondedAt null.Time `json:"responded_at"` @@ -78,8 +79,8 @@ type ResponseReadPrivilegeInfo struct { IsRespondent bool } -//InsertQuestionnaire アンケートの追加 -func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string) (int, error) { +// InsertQuestionnaire アンケートの追加 +func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isAnonymous bool) (int, error) { db, err := getTx(ctx) if err != nil { return 0, fmt.Errorf("failed to get tx: %w", err) @@ -91,6 +92,7 @@ func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, des Title: title, Description: description, ResSharedTo: resSharedTo, + IsAnonymous: isAnonymous, } } else { questionnaire = Questionnaires{ @@ -98,6 +100,7 @@ func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, des Description: description, ResTimeLimit: resTimeLimit, ResSharedTo: resSharedTo, + IsAnonymous: isAnonymous, } } @@ -109,8 +112,8 @@ func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, des return questionnaire.ID, nil } -//UpdateQuestionnaire アンケートの更新 -func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int) error { +// UpdateQuestionnaire アンケートの更新 +func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isAnonymous bool, questionnaireID int) error { db, err := getTx(ctx) if err != nil { return fmt.Errorf("failed to get tx: %w", err) @@ -123,6 +126,7 @@ func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, des Description: description, ResTimeLimit: resTimeLimit, ResSharedTo: resSharedTo, + IsAnonymous: isAnonymous, } } else { questionnaire = map[string]interface{}{ @@ -130,6 +134,7 @@ func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, des "description": description, "res_time_limit": gorm.Expr("NULL"), "res_shared_to": resSharedTo, + "is_anonymous": isAnonymous, } } @@ -148,7 +153,7 @@ func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, des return nil } -//DeleteQuestionnaire アンケートの削除 +// DeleteQuestionnaire アンケートの削除 func (*Questionnaire) DeleteQuestionnaire(ctx context.Context, questionnaireID int) error { db, err := getTx(ctx) if err != nil { @@ -167,8 +172,10 @@ func (*Questionnaire) DeleteQuestionnaire(ctx context.Context, questionnaireID i return nil } -/*GetQuestionnaires アンケートの一覧 -2つ目の戻り値はページ数の最大値*/ +/* +GetQuestionnaires アンケートの一覧 +2つ目の戻り値はページ数の最大値 +*/ func (*Questionnaire) GetQuestionnaires(ctx context.Context, userID string, sort string, search string, pageNum int, nontargeted bool) ([]QuestionnaireInfo, int, error) { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() @@ -263,7 +270,7 @@ func (*Questionnaire) GetAdminQuestionnaires(ctx context.Context, userID string) return questionnaires, nil } -//GetQuestionnaireInfo アンケートの詳細な情報取得 +// GetQuestionnaireInfo アンケートの詳細な情報取得 func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID int) (*Questionnaires, []string, []string, []string, error) { db, err := getTx(ctx) if err != nil { @@ -315,7 +322,7 @@ func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID return &questionnaire, targets, administrators, respondents, nil } -//GetTargettedQuestionnaires targetになっているアンケートの取得 +// GetTargettedQuestionnaires targetになっているアンケートの取得 func (*Questionnaire) GetTargettedQuestionnaires(ctx context.Context, userID string, answered string, sort string) ([]TargettedQuestionnaire, error) { db, err := getTx(ctx) if err != nil { @@ -359,7 +366,7 @@ func (*Questionnaire) GetTargettedQuestionnaires(ctx context.Context, userID str return questionnaires, nil } -//GetQuestionnaireLimit アンケートの回答期限の取得 +// GetQuestionnaireLimit アンケートの回答期限の取得 func (*Questionnaire) GetQuestionnaireLimit(ctx context.Context, questionnaireID int) (null.Time, error) { db, err := getTx(ctx) if err != nil { @@ -431,6 +438,28 @@ func (*Questionnaire) GetResponseReadPrivilegeInfoByResponseID(ctx context.Conte return &responseReadPrivilegeInfo, nil } +func (*Questionnaire) GetResponseAnonymousByQuestionnaireID(ctx context.Context, questionnaireID int) (bool, error) { + db, err := getTx(ctx) + if err != nil { + return true, fmt.Errorf("failed to get tx: %w", err) + } + + var is_anonymous bool + err = db. + Table("questionnaires"). + Where("questionnaires.id = ?", questionnaireID). + Select("questionnaires.is_anonymous"). + Take(&is_anonymous).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return true, ErrRecordNotFound + } + if err != nil { + return true, fmt.Errorf("failed to get response read privilege info: %w", err) + } + + return is_anonymous, nil +} + func (*Questionnaire) GetResponseReadPrivilegeInfoByQuestionnaireID(ctx context.Context, userID string, questionnaireID int) (*ResponseReadPrivilegeInfo, error) { db, err := getTx(ctx) if err != nil { diff --git a/model/questionnaires_test.go b/model/questionnaires_test.go index d8a3b3d8..a809036f 100644 --- a/model/questionnaires_test.go +++ b/model/questionnaires_test.go @@ -355,6 +355,7 @@ func insertQuestionnaireTest(t *testing.T) { description string resTimeLimit null.Time resSharedTo string + isAnonymous bool } type expect struct { isErr bool @@ -375,6 +376,7 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -384,6 +386,7 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -393,6 +396,7 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "respondents", + isAnonymous: false, }, }, { @@ -402,6 +406,7 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "administrators", + isAnonymous: false, }, }, { @@ -411,6 +416,7 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -420,6 +426,7 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, expect: expect{ isErr: true, @@ -432,6 +439,7 @@ func insertQuestionnaireTest(t *testing.T) { description: strings.Repeat("a", 2000), resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -441,17 +449,28 @@ func insertQuestionnaireTest(t *testing.T) { description: strings.Repeat("a", 200000), resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, expect: expect{ isErr: true, }, }, + { + description: "anonymous: true", + args: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + resTimeLimit: null.NewTime(time.Time{}, false), + resSharedTo: "public", + isAnonymous: true, + }, + }, } for _, testCase := range testCases { ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, testCase.args.title, testCase.args.description, testCase.args.resTimeLimit, testCase.args.resSharedTo) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, testCase.args.title, testCase.args.description, testCase.args.resTimeLimit, testCase.args.resSharedTo, testCase.isAnonymous) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -492,6 +511,7 @@ func updateQuestionnaireTest(t *testing.T) { description string resTimeLimit null.Time resSharedTo string + isAnonymous bool } type expect struct { isErr bool @@ -513,12 +533,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "respondents", + isAnonymous: false, }, }, { @@ -528,12 +550,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第2回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -543,12 +567,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第2回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -558,12 +584,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "respondents", + isAnonymous: false, }, }, { @@ -573,12 +601,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第2回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -588,12 +618,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第2回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -603,12 +635,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -618,12 +652,14 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now().Add(time.Minute), true), resSharedTo: "public", + isAnonymous: false, }, }, { @@ -633,12 +669,31 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, + }, + }, + { + description: "update is_anonymous(false->true)", + before: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + resTimeLimit: null.NewTime(time.Now(), true), + resSharedTo: "public", + isAnonymous: false, + }, + after: args{ + title: "第2回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + resTimeLimit: null.NewTime(time.Now(), true), + resSharedTo: "public", + isAnonymous: true, }, }, } @@ -663,7 +718,7 @@ func updateQuestionnaireTest(t *testing.T) { createdAt := questionnaire.CreatedAt questionnaireID := questionnaire.ID after := &testCase.after - err = questionnaireImpl.UpdateQuestionnaire(ctx, after.title, after.description, after.resTimeLimit, after.resSharedTo, questionnaireID) + err = questionnaireImpl.UpdateQuestionnaire(ctx, after.title, after.description, after.resTimeLimit, after.resSharedTo, false, questionnaireID) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -715,19 +770,21 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, { title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isAnonymous: false, }, } for _, arg := range invalidTestCases { ctx := context.Background() - err := questionnaireImpl.UpdateQuestionnaire(ctx, arg.title, arg.description, arg.resTimeLimit, arg.resSharedTo, invalidQuestionnaireID) + err := questionnaireImpl.UpdateQuestionnaire(ctx, arg.title, arg.description, arg.resTimeLimit, arg.resSharedTo, arg.isAnonymous, invalidQuestionnaireID) if !errors.Is(err, ErrNoRecordUpdated) { if err == nil { t.Errorf("Succeeded with invalid questionnaireID") @@ -749,6 +806,7 @@ func deleteQuestionnaireTest(t *testing.T) { description string resTimeLimit null.Time resSharedTo string + isAnonymous bool } type expect struct { isErr bool @@ -766,6 +824,7 @@ func deleteQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isAnonymous: false, }, }, } @@ -778,6 +837,7 @@ func deleteQuestionnaireTest(t *testing.T) { Description: testCase.args.description, ResTimeLimit: testCase.args.resTimeLimit, ResSharedTo: testCase.args.resSharedTo, + IsAnonymous: testCase.args.isAnonymous, } err := db. Session(&gorm.Session{NewDB: true}). diff --git a/model/respondents.go b/model/respondents.go index 67504296..56f10c49 100644 --- a/model/respondents.go +++ b/model/respondents.go @@ -17,6 +17,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) ([]RespondentDetail, error) + GetAnonymousRespondentDetails(ctx context.Context, questionnaireID int, sort string) ([]AnonymousRespondentDetail, error) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs []int) ([]Respondents, error) CheckRespondent(ctx context.Context, userID string, questionnaireID int) (bool, error) } diff --git a/model/respondents_impl.go b/model/respondents_impl.go index 29019230..432624d1 100755 --- a/model/respondents_impl.go +++ b/model/respondents_impl.go @@ -65,6 +65,15 @@ type RespondentDetail struct { Responses []ResponseBody `json:"body"` } +// AnonymousRespondentDetail 匿名の回答の詳細情報の構造体 +type AnonymousRespondentDetail struct { + ResponseID int `json:"responseID,omitempty"` + QuestionnaireID int `json:"questionnaireID,omitempty"` + SubmittedAt null.Time `json:"submitted_at,omitempty"` + ModifiedAt time.Time `json:"modified_at,omitempty"` + Responses []ResponseBody `json:"body"` +} + // InsertRespondent 回答の追加 func (*Respondent) InsertRespondent(ctx context.Context, userID string, questionnaireID int, submittedAt null.Time) (int, error) { db, err := getTx(ctx) @@ -362,6 +371,111 @@ func (*Respondent) GetRespondentDetails(ctx context.Context, questionnaireID int return respondentDetails, nil } +// GetAnonymousRespondentDetails アンケートの回答の匿名詳細情報一覧の取得 +func (*Respondent) GetAnonymousRespondentDetails(ctx context.Context, questionnaireID int, sort string) ([]AnonymousRespondentDetail, error) { + db, err := getTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tx: %w", err) + } + + respondents := []Respondents{} + + // Note: respondents.submitted_at IS NOT NULLで一時保存の回答を除外している + query := db. + Session(&gorm.Session{}). + Where("respondents.questionnaire_id = ? AND respondents.submitted_at IS NOT NULL", questionnaireID). + Select("ResponseID", "ModifiedAt", "SubmittedAt") + + query, sortNum, err := setRespondentsOrder(query, sort) + if err != nil { + return nil, fmt.Errorf("failed to set order: %w", err) + } + + err = query. + Find(&respondents).Error + if err != nil { + return nil, fmt.Errorf("failed to get respondents: %w", err) + } + + if len(respondents) == 0 { + return []AnonymousRespondentDetail{}, nil + } + + responseIDs := make([]int, 0, len(respondents)) + for _, respondent := range respondents { + responseIDs = append(responseIDs, respondent.ResponseID) + } + + respondentDetails := make([]AnonymousRespondentDetail, 0, len(respondents)) + respondentDetailMap := make(map[int]*AnonymousRespondentDetail, len(respondents)) + for i, respondent := range respondents { + respondentDetails = append(respondentDetails, AnonymousRespondentDetail{ + ResponseID: respondent.ResponseID, + QuestionnaireID: questionnaireID, + SubmittedAt: respondent.SubmittedAt, + ModifiedAt: respondent.ModifiedAt, + }) + + respondentDetailMap[respondent.ResponseID] = &respondentDetails[i] + } + + 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 = ?", questionnaireID). + Order("question_num"). + Select("ID", "Type"). + Find(&questions).Error + if err != nil { + return nil, fmt.Errorf("failed to get questions: %w", err) + } + + for _, question := range questions { + responseBodyMap := make(map[int][]string, len(respondents)) + for _, response := range question.Responses { + if response.Body.Valid { + responseBodyMap[response.ResponseID] = append(responseBodyMap[response.ResponseID], response.Body.String) + } + } + + for i := range respondentDetails { + responseBodies := responseBodyMap[respondentDetails[i].ResponseID] + responseBody := ResponseBody{ + QuestionID: question.ID, + QuestionType: question.Type, + } + + switch responseBody.QuestionType { + 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) + } + } + + respondentDetails[i].Responses = append(respondentDetails[i].Responses, responseBody) + } + } + + respondentDetails, err = sortAnonymousRespondentDetail(sortNum, len(questions), respondentDetails) + if err != nil { + return nil, fmt.Errorf("failed to sort RespondentDetails: %w", err) + } + + return respondentDetails, nil +} + // GetRespondentsUserIDs 回答者のユーザーID取得 func (*Respondent) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs []int) ([]Respondents, error) { db, err := getTx(ctx) @@ -483,3 +597,60 @@ func sortRespondentDetail(sortNum int, questionNum int, respondentDetails []Resp return respondentDetails, nil } + +func sortAnonymousRespondentDetail(sortNum int, questionNum int, respondentDetails []AnonymousRespondentDetail) ([]AnonymousRespondentDetail, error) { + if sortNum == 0 { + return respondentDetails, nil + } + sortNumAbs := int(math.Abs(float64(sortNum))) + if sortNumAbs > questionNum { + return nil, fmt.Errorf("sort param is too large: %d", sortNum) + } + + sort.Slice(respondentDetails, func(i, j int) bool { + bodyI := respondentDetails[i].Responses[sortNumAbs-1] + bodyJ := respondentDetails[j].Responses[sortNumAbs-1] + if bodyI.QuestionType == "Number" { + numi, err := strconv.ParseFloat(bodyI.Body.String, 64) + if err != nil { + return true + } + numj, err := strconv.ParseFloat(bodyJ.Body.String, 64) + if err != nil { + return true + } + if sortNum < 0 { + return numi > numj + } + return numi < numj + } + if bodyI.QuestionType == "MultipleChoice" { + choiceI := "" + if len(bodyI.OptionResponse) > 0 { + choiceI = bodyI.OptionResponse[0] + } + choiceJ := "" + if len(bodyJ.OptionResponse) > 0 { + choiceJ = bodyJ.OptionResponse[0] + } + if sortNum < 0 { + return choiceI > choiceJ + } + return choiceI < choiceJ + } + if bodyI.QuestionType == "Checkbox" { + selectionsI := strings.Join(bodyI.OptionResponse, ", ") + selectionsJ := strings.Join(bodyJ.OptionResponse, ", ") + if sortNum < 0 { + return selectionsI > selectionsJ + } + return selectionsI < selectionsJ + } + if sortNum < 0 { + return bodyI.Body.String > bodyJ.Body.String + } + return bodyI.Body.String < bodyJ.Body.String + }) + + return respondentDetails, nil +} diff --git a/model/respondents_test.go b/model/respondents_test.go index cafc7dd7..ebaccc84 100644 --- a/model/respondents_test.go +++ b/model/respondents_test.go @@ -19,7 +19,7 @@ func TestInsertRespondent(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -129,7 +129,7 @@ func TestUpdateSubmittedAt(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -203,7 +203,7 @@ func TestDeleteRespondent(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -390,9 +390,9 @@ func TestGetRespondentInfos(t *testing.T) { args expect } - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) - questionnaireID2, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID2, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) questionnaire := Questionnaires{} @@ -522,7 +522,7 @@ func TestGetRespondentDetail(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", false) require.NoError(t, err) questionnaire := Questionnaires{} @@ -619,7 +619,7 @@ func TestGetRespondentDetails(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", false) require.NoError(t, err) questionnaire := Questionnaires{} @@ -908,7 +908,7 @@ func TestGetRespondentsUserIDs(t *testing.T) { } questionnaireIDs := make([]int, 0, 3) for i := 0; i < 3; i++ { - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) questionnaireIDs = append(questionnaireIDs, questionnaireID) } @@ -996,7 +996,7 @@ func TestTestCheckRespondent(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/responses_test.go b/model/responses_test.go index 9790b350..aa753311 100644 --- a/model/responses_test.go +++ b/model/responses_test.go @@ -19,7 +19,7 @@ func TestInsertResponses(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -142,7 +142,7 @@ func TestDeleteResponse(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/scale_labels_test.go b/model/scale_labels_test.go index f4f79ccd..52470a13 100644 --- a/model/scale_labels_test.go +++ b/model/scale_labels_test.go @@ -20,7 +20,7 @@ func TestInsertScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -163,7 +163,7 @@ func TestUpdateScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -283,7 +283,7 @@ func TestDeleteScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -371,7 +371,7 @@ func TestGetScaleLabels(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -489,7 +489,7 @@ func TestCheckScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/validations_test.go b/model/validations_test.go index a56434a3..6e6e3fca 100644 --- a/model/validations_test.go +++ b/model/validations_test.go @@ -20,7 +20,7 @@ func TestInsertValidation(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -212,7 +212,7 @@ func TestUpdateValidation(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -360,7 +360,7 @@ func TestDeleteValidation(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -458,7 +458,7 @@ func TestGetValidations(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/router.go b/router.go index 389a4d19..4c8a35c4 100644 --- a/router.go +++ b/router.go @@ -40,6 +40,8 @@ func SetRouting(port string) { apiQuestionnnaires.DELETE("/:questionnaireID", api.DeleteQuestionnaire, api.QuestionnaireAdministratorAuthenticate) apiQuestionnnaires.GET("/:questionnaireID/questions", api.GetQuestions) apiQuestionnnaires.POST("/:questionnaireID/questions", api.PostQuestionByQuestionnaireID) + apiQuestionnnaires.GET("/:questionnaireID/responses", api.GetResponses, api.AnonymousAuthenticate) + apiQuestionnnaires.GET("/:questionnaireID/results", api.GetResults) } apiQuestions := echoAPI.Group("/questions") @@ -73,10 +75,10 @@ func SetRouting(port string) { apiUsers.GET("/:traQID/targeted", api.GetTargettedQuestionnairesBytraQID) } - apiResults := echoAPI.Group("/results") - { - apiResults.GET("/:questionnaireID", api.GetResults, api.ResultAuthenticate) - } + // apiResults := echoAPI.Group("/results") + // { + // apiResults.GET("/:questionnaireID", api.GetResults, api.ResultAuthenticate) + // } } e.Logger.Fatal(e.Start(port)) diff --git a/router/middleware.go b/router/middleware.go index c1ee10f5..a4d88bd8 100644 --- a/router/middleware.go +++ b/router/middleware.go @@ -47,8 +47,11 @@ func (*Middleware) SetValidatorMiddleware(next echo.HandlerFunc) echo.HandlerFun } } -/* 消せないアンケートの発生を防ぐための管理者 -暫定的にハードコーディングで対応*/ +/* + 消せないアンケートの発生を防ぐための管理者 + +暫定的にハードコーディングで対応 +*/ var adminUserIDs = []string{"ryoha", "xxarupakaxx", "kaitoyama", "cp20", "itzmeowww"} // SetUserIDMiddleware X-Showcase-UserからユーザーIDを取得しセットする @@ -282,6 +285,38 @@ func (m *Middleware) QuestionAdministratorAuthenticate(next echo.HandlerFunc) ec } } +// AnonymousAuthenticate アンケートが匿名かどうかの認証 +func (m *Middleware) AnonymousAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + _, err := getUserID(c) + if err != nil { + c.Logger().Errorf("failed to get userID: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) + } + strQuestionnaireID := c.Param("questionnaireID") + questionnaireID, err := strconv.Atoi(strQuestionnaireID) + if err != nil { + c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid questionnaireID:%s(error: %w)", strQuestionnaireID, err)) + } + + isAnonymous, err := m.GetResponseAnonymousByQuestionnaireID(c.Request().Context(), questionnaireID) + if errors.Is(err, model.ErrRecordNotFound) { + c.Logger().Infof("response not found: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid responseID: %d", questionnaireID)) + } else if err != nil { + c.Logger().Errorf("failed to get responseReadPrivilegeInfo: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get response read privilege info: %w", err)) + } + + if isAnonymous { + return echo.NewHTTPError(http.StatusForbidden, "You do not have permission to view this response.") + } + return next(c) + } + +} + // ResultAuthenticate アンケートの回答を確認できるかの認証 func (m *Middleware) ResultAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { diff --git a/router/questionnaires.go b/router/questionnaires.go index b33db7f4..33331591 100644 --- a/router/questionnaires.go +++ b/router/questionnaires.go @@ -145,6 +145,7 @@ type PostAndEditQuestionnaireRequest struct { Description string `json:"description"` ResTimeLimit null.Time `json:"res_time_limit"` ResSharedTo string `json:"res_shared_to" validate:"required,oneof=administrators respondents public"` + IsAnonymous bool `json:"is_anonymous" validate:"required"` Targets []string `json:"targets" validate:"dive,max=32"` Administrators []string `json:"administrators" validate:"required,min=1,dive,max=32"` } @@ -182,7 +183,7 @@ func (q *Questionnaire) PostQuestionnaire(c echo.Context) error { var questionnaireID int err = q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { - questionnaireID, err = q.InsertQuestionnaire(ctx, req.Title, req.Description, req.ResTimeLimit, req.ResSharedTo) + questionnaireID, err = q.InsertQuestionnaire(ctx, req.Title, req.Description, req.ResTimeLimit, req.ResSharedTo, req.IsAnonymous) if err != nil { c.Logger().Errorf("failed to insert a questionnaire: %+v", err) return err @@ -200,19 +201,19 @@ func (q *Questionnaire) PostQuestionnaire(c echo.Context) error { return err } - message := createQuestionnaireMessage( - questionnaireID, - req.Title, - req.Description, - req.Administrators, - req.ResTimeLimit, - req.Targets, - ) - err = q.PostMessage(message) - if err != nil { - c.Logger().Errorf("failed to post message: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, "failed to post message to traQ") - } + // message := createQuestionnaireMessage( + // questionnaireID, + // req.Title, + // req.Description, + // req.Administrators, + // req.ResTimeLimit, + // req.Targets, + // ) + // err = q.PostMessage(message) + // if err != nil { + // c.Logger().Errorf("failed to post message: %+v", err) + // return echo.NewHTTPError(http.StatusInternalServerError, "failed to post message to traQ") + // } return nil }) @@ -236,6 +237,7 @@ func (q *Questionnaire) PostQuestionnaire(c echo.Context) error { "created_at": now.Format(time.RFC3339), "modified_at": now.Format(time.RFC3339), "res_shared_to": req.ResSharedTo, + "is_anonymous": req.IsAnonymous, "targets": req.Targets, "administrators": req.Administrators, }) @@ -268,6 +270,7 @@ func (q *Questionnaire) GetQuestionnaire(c echo.Context) error { "created_at": questionnaire.CreatedAt.Format(time.RFC3339), "modified_at": questionnaire.ModifiedAt.Format(time.RFC3339), "res_shared_to": questionnaire.ResSharedTo, + "is_anonymous": questionnaire.IsAnonymous, "targets": targets, "administrators": administrators, "respondents": respondents, @@ -409,7 +412,7 @@ func (q *Questionnaire) EditQuestionnaire(c echo.Context) error { } err = q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { - err = q.UpdateQuestionnaire(ctx, req.Title, req.Description, req.ResTimeLimit, req.ResSharedTo, questionnaireID) + err = q.UpdateQuestionnaire(ctx, req.Title, req.Description, req.ResTimeLimit, req.ResSharedTo, req.IsAnonymous, questionnaireID) if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { c.Logger().Errorf("failed to update questionnaire: %+v", err) return err @@ -628,6 +631,43 @@ func (q *Questionnaire) GetQuestions(c echo.Context) error { return c.JSON(http.StatusOK, ret) } +// GetResponses GET /questionnaires/:questionnaireID/responses +func (r *Result) GetResponses(c echo.Context) error { + print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + sort := c.QueryParam("sort") + questionnaireID, err := strconv.Atoi(c.Param("questionnaireID")) + if err != nil { + c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest) + } + + respondentDetails, err := r.GetRespondentDetails(c.Request().Context(), questionnaireID, sort) + if err != nil { + c.Logger().Errorf("failed to get respondent details: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, err) + } + + return c.JSON(http.StatusOK, respondentDetails) +} + +// GetResults /questionnaires/:questionnaireID/result +func (r *Result) GetResults(c echo.Context) error { + sort := c.QueryParam("sort") + questionnaireID, err := strconv.Atoi(c.Param("questionnaireID")) + if err != nil { + c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest) + } + + respondentDetails, err := r.GetAnonymousRespondentDetails(c.Request().Context(), questionnaireID, sort) + if err != nil { + c.Logger().Errorf("failed to get respondent details: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, err) + } + + return c.JSON(http.StatusOK, respondentDetails) +} + func createQuestionnaireMessage(questionnaireID int, title string, description string, administrators []string, resTimeLimit null.Time, targets []string) string { var resTimeLimitText string if resTimeLimit.Valid { diff --git a/router/results.go b/router/results.go index 829c20f0..e8ecf6c1 100644 --- a/router/results.go +++ b/router/results.go @@ -1,10 +1,6 @@ package router import ( - "net/http" - "strconv" - - "github.com/labstack/echo/v4" "github.com/traPtitech/anke-to/model" ) @@ -23,21 +19,3 @@ func NewResult(respondent model.IRespondent, questionnaire model.IQuestionnaire, IAdministrator: administrator, } } - -// GetResults GET /results/:questionnaireID -func (r *Result) GetResults(c echo.Context) error { - sort := c.QueryParam("sort") - questionnaireID, err := strconv.Atoi(c.Param("questionnaireID")) - if err != nil { - c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - respondentDetails, err := r.GetRespondentDetails(c.Request().Context(), questionnaireID, sort) - if err != nil { - c.Logger().Errorf("failed to get respondent details: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, respondentDetails) -} From 55949417090df8ad29c7b0ceb9595cd26ec2815a Mon Sep 17 00:00:00 2001 From: kaitoyama Date: Tue, 19 Dec 2023 22:07:14 +0900 Subject: [PATCH 2/4] remove comment out for dev --- router/questionnaires.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/router/questionnaires.go b/router/questionnaires.go index 33331591..9e9cb1ac 100644 --- a/router/questionnaires.go +++ b/router/questionnaires.go @@ -201,19 +201,19 @@ func (q *Questionnaire) PostQuestionnaire(c echo.Context) error { return err } - // message := createQuestionnaireMessage( - // questionnaireID, - // req.Title, - // req.Description, - // req.Administrators, - // req.ResTimeLimit, - // req.Targets, - // ) - // err = q.PostMessage(message) - // if err != nil { - // c.Logger().Errorf("failed to post message: %+v", err) - // return echo.NewHTTPError(http.StatusInternalServerError, "failed to post message to traQ") - // } + message := createQuestionnaireMessage( + questionnaireID, + req.Title, + req.Description, + req.Administrators, + req.ResTimeLimit, + req.Targets, + ) + err = q.PostMessage(message) + if err != nil { + c.Logger().Errorf("failed to post message: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to post message to traQ") + } return nil }) From d1e874bef81a71519315bb5027b6bc7db2e55dec Mon Sep 17 00:00:00 2001 From: kaitoyama Date: Tue, 19 Dec 2023 22:19:41 +0900 Subject: [PATCH 3/4] delete debug log --- router/questionnaires.go | 1 - 1 file changed, 1 deletion(-) diff --git a/router/questionnaires.go b/router/questionnaires.go index 9e9cb1ac..24db1e15 100644 --- a/router/questionnaires.go +++ b/router/questionnaires.go @@ -633,7 +633,6 @@ func (q *Questionnaire) GetQuestions(c echo.Context) error { // GetResponses GET /questionnaires/:questionnaireID/responses func (r *Result) GetResponses(c echo.Context) error { - print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") sort := c.QueryParam("sort") questionnaireID, err := strconv.Atoi(c.Param("questionnaireID")) if err != nil { From a414e1616679023c7f4f4cef3f398d5d1a0b79ef Mon Sep 17 00:00:00 2001 From: kaitoyama Date: Wed, 20 Dec 2023 10:19:12 +0900 Subject: [PATCH 4/4] Add response anonymity check in middleware --- router/middleware.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/router/middleware.go b/router/middleware.go index a4d88bd8..9b70eaf1 100644 --- a/router/middleware.go +++ b/router/middleware.go @@ -177,6 +177,17 @@ func (m *Middleware) ResponseReadAuthenticate(next echo.HandlerFunc) echo.Handle return next(c) } + isAnonymous, err := m.GetResponseAnonymousByQuestionnaireID(c.Request().Context(), respondent.QuestionnaireID) + if err != nil { + c.Logger().Errorf("failed to get response anonymous info: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get response anonymous info: %w", err)) + } + + // 匿名なら回答者以外は閲覧できない + if isAnonymous { + return c.String(http.StatusForbidden, "You do not have permission to view this response.") + } + // 回答者以外は一時保存の回答は閲覧できない if !respondent.SubmittedAt.Valid { c.Logger().Info("not submitted")