diff --git a/docs/spec/components/schemas/DailyQuestionCreate.yaml b/docs/spec/components/schemas/DailyQuestionCreate.yaml new file mode 100644 index 0000000..325ca9d --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionCreate.yaml @@ -0,0 +1,55 @@ +allOf: + - $ref: "#/components/schemas/DailyQuestionEditKey" + - type: object + required: + - attributes + properties: + attributes: + type: object + required: + - title + - reward + - options + - correct_answer + - time_for_answer + - starts_at + properties: + title: + type: string + description: Question title + example: Georgian capital + reward: + type: integer + format: int64 + description: Reward for a correct answer + options: + type: array + description: Answer options. Minimum 2, maximum 6 + items: + $ref: "#/components/schemas/DailyQuestionOptions" + example: [ + { + "id": 0, + "title": "" + }, + { + "id": 1, + "title": "" + }, + { + "id": 2, + "title": "" + } + ] + correct_answer: + type: integer + format: int64 + description: Correct answer ID + time_for_answer: + type: integer + format: int64 + description: Time for answer + starts_at: + type: string + description: Start date when this question is available, hours and minutes are always 0 + example: "2024-08-23" \ No newline at end of file diff --git a/docs/spec/components/schemas/DailyQuestionCreateKey.yaml b/docs/spec/components/schemas/DailyQuestionCreateKey.yaml new file mode 100644 index 0000000..9a4e05f --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionCreateKey.yaml @@ -0,0 +1,11 @@ +type: object +required: + - id + - type +properties: + id: + type: string + description: Question id + type: + type: string + enum: [ daily_questions ] diff --git a/docs/spec/components/schemas/DailyQuestionDel.yaml b/docs/spec/components/schemas/DailyQuestionDel.yaml new file mode 100644 index 0000000..e43e232 --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionDel.yaml @@ -0,0 +1,15 @@ +allOf: + - $ref: "#/components/schemas/DailyQuestionDelKey" + - type: object + required: + - attributes + properties: + attributes: + type: object + required: + - title + properties: + title: + type: string + description: Question title + example: Georgian capital \ No newline at end of file diff --git a/docs/spec/components/schemas/DailyQuestionDelKey.yaml b/docs/spec/components/schemas/DailyQuestionDelKey.yaml new file mode 100644 index 0000000..9a4e05f --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionDelKey.yaml @@ -0,0 +1,11 @@ +type: object +required: + - id + - type +properties: + id: + type: string + description: Question id + type: + type: string + enum: [ daily_questions ] diff --git a/docs/spec/components/schemas/DailyQuestionDetails.yaml b/docs/spec/components/schemas/DailyQuestionDetails.yaml new file mode 100644 index 0000000..628b8ed --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionDetails.yaml @@ -0,0 +1,77 @@ +allOf: + - $ref: "#/components/schemas/DailyQuestionDetailsKey" + - type: object + required: + - attributes + properties: + attributes: + type: object + required: + - title + - reward + - options + - correct_answer + - time_for_answer + - starts_at + - created_at + - num_correct_answers + - num_incorrect_answers + - num_all_participants + properties: + title: + type: string + description: Question title + example: Georgian capital + reward: + type: integer + format: int64 + description: Reward for a correct answer + options: + type: array + description: Answer options. Minimum 2, maximum 6 + items: + $ref: "#/components/schemas/DailyQuestionOptions" + example: [ + { + "id": 0, + "title": "" + }, + { + "id": 1, + "title": "" + }, + { + "id": 2, + "title": "" + } + ] + correct_answer: + type: integer + format: int64 + description: Correct answer ID + time_for_answer: + type: integer + format: int64 + description: Time for answer + starts_at: + type: string + description: Start date when this question is available, hours and minutes are always 0 + example: "2024-08-26T00:00:00Z" + created_at: + type: string + description: Start date when this question was create + example: "2024-08-26T00:00:00Z" + num_correct_answers: + type: integer + format: int64 + description: Number of correct answers + num_incorrect_answers: + type: integer + format: int64 + description: Number of incorrect answers + num_all_participants: + type: integer + format: int64 + description: | + Users who received the question, those who answered and + those who did not answer in the time given to them diff --git a/docs/spec/components/schemas/DailyQuestionDetailsKey.yaml b/docs/spec/components/schemas/DailyQuestionDetailsKey.yaml new file mode 100644 index 0000000..9a4e05f --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionDetailsKey.yaml @@ -0,0 +1,11 @@ +type: object +required: + - id + - type +properties: + id: + type: string + description: Question id + type: + type: string + enum: [ daily_questions ] diff --git a/docs/spec/components/schemas/DailyQuestionEdit.yaml b/docs/spec/components/schemas/DailyQuestionEdit.yaml new file mode 100644 index 0000000..19d3363 --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionEdit.yaml @@ -0,0 +1,48 @@ +allOf: + - $ref: "#/components/schemas/DailyQuestionEditKey" + - type: object + required: + - attributes + properties: + attributes: + type: object + properties: + title: + type: string + description: Question title + example: Georgian capital + reward: + type: integer + format: int64 + description: Reward for a correct answer + options: + type: array + description: Answer options. Minimum 2, maximum 6 + items: + $ref: "#/components/schemas/DailyQuestionOptions" + example: [ + { + "id": 0, + "title": "" + }, + { + "id": 1, + "title": "" + }, + { + "id": 2, + "title": "" + } + ] + correct_answer: + type: integer + format: int64 + description: Correct answer ID + time_for_answer: + type: integer + format: int64 + description: Time for answer + starts_at: + type: string + description: Start date when this question is available, hours and minutes are always 0 + example: "2024-08-23" \ No newline at end of file diff --git a/docs/spec/components/schemas/DailyQuestionEditKey.yaml b/docs/spec/components/schemas/DailyQuestionEditKey.yaml new file mode 100644 index 0000000..9a4e05f --- /dev/null +++ b/docs/spec/components/schemas/DailyQuestionEditKey.yaml @@ -0,0 +1,11 @@ +type: object +required: + - id + - type +properties: + id: + type: string + description: Question id + type: + type: string + enum: [ daily_questions ] diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@daily_questions@admin.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@daily_questions@admin.yaml new file mode 100644 index 0000000..fec2cc3 --- /dev/null +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@daily_questions@admin.yaml @@ -0,0 +1,69 @@ +post: + tags: + - Daily Questions + summary: Create daily question + description: | + Create Daily Question user must be superuser + operationId: createDailyQuestion + security: + - BearerAuth: [] + requestBody: + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/DailyQuestionCreate' + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/DailyQuestionDetails' + 400: + $ref: '#/components/responses/invalidParameter' + 401: + $ref: '#/components/responses/invalidAuth' + 409: + description: On this day, the daily question already exists + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 500: + $ref: '#/components/responses/internalError' + +get: + tags: + - Daily Questions + summary: Filter Daily Question by start + description: | + Filtering of daily questions by their activation time + operationId: filterStartAtDailyQuestion + security: + - BearerAuth: [] + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/DailyQuestionDetails' + 500: + $ref: '#/components/responses/internalError' \ No newline at end of file diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@daily_questions@admin@{question_id}.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@daily_questions@admin@{question_id}.yaml new file mode 100644 index 0000000..6a41a74 --- /dev/null +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@daily_questions@admin@{question_id}.yaml @@ -0,0 +1,77 @@ +delete: + tags: + - Daily Questions + summary: Delete daily question + description: | + Delete Daily Question user must be superuser + operationId: deleteDailyQuestion + security: + - BearerAuth: [] + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/DailyQuestionDetails' + 204: + description: No content + 400: + $ref: '#/components/responses/invalidParameter' + 404: + description: Question with ID not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 500: + $ref: '#/components/responses/internalError' + +patch: + tags: + - Daily Questions + summary: Edit daily question + description: | + Edit Daily Question user must be superuser + operationId: editDailyQuestion + security: + - BearerAuth: [] + requestBody: + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/DailyQuestionEdit' + responses: + 204: + description: No content + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/DailyQuestionDetails' + 400: + $ref: '#/components/responses/invalidParameter' + 409: + description: On this day, the daily question already exists + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 500: + $ref: '#/components/responses/internalError' \ No newline at end of file diff --git a/go.mod b/go.mod index 95915f4..1e7dcf0 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( gitlab.com/distributed_lab/logan v3.8.1+incompatible gitlab.com/distributed_lab/running v1.6.0 gitlab.com/distributed_lab/urlval/v4 v4.0.3 - gitlab.com/tokend/go v3.16.0+incompatible ) require ( @@ -111,7 +110,6 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mtibben/percent v0.2.1 // indirect - github.com/nullstyle/go-xdr v0.0.0-20180726165426-f4c839f75077 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect diff --git a/go.sum b/go.sum index 5201301..761f336 100644 --- a/go.sum +++ b/go.sum @@ -2117,8 +2117,6 @@ github.com/rarimo/geo-auth-svc v1.1.0 h1:3k1tTWAjtCBsnzlMb3aB+xgsFLEPUSmB3woME+q github.com/rarimo/geo-auth-svc v1.1.0/go.mod h1:JrpCGdT0xtAcWIKgPhxPHf7QCW4h845BXuh6M7NdQFw= github.com/rarimo/saver-grpc-lib v1.0.0 h1:MGUVjYg7unmodYczVsLqlqZNkT4CIgKqdo6aQtL1qdE= github.com/rarimo/saver-grpc-lib v1.0.0/go.mod h1:DpugWK5B7Hi0bdC3MPe/9FD2zCxaRwsyykdwxtF1Zgg= -github.com/rarimo/zkverifier-kit v1.2.0 h1:Qsdcq+jMBEkdTlbqGT7InQhNI39lZyCX9PXgqzb1ozM= -github.com/rarimo/zkverifier-kit v1.2.0/go.mod h1:3YDg5dTkDRr4IdfaDHGYetopd6gS/2SuwSeseYTWwNw= github.com/rarimo/zkverifier-kit v1.2.2 h1:pezzkNNjz6gSzfiR5eP+fcvB/UOkYWPaRdnkA7RaGPE= github.com/rarimo/zkverifier-kit v1.2.2/go.mod h1:3YDg5dTkDRr4IdfaDHGYetopd6gS/2SuwSeseYTWwNw= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= diff --git a/internal/data/daily_questions.go b/internal/data/daily_questions.go index dcbccd2..b9e32d5 100644 --- a/internal/data/daily_questions.go +++ b/internal/data/daily_questions.go @@ -7,12 +7,19 @@ import ( "time" "github.com/rarimo/geo-points-svc/resources" + "gitlab.com/distributed_lab/kit/pgdb" ) const ( - ColCorrectAnswers = "num_correct_answers" - ColIncorrectAnswers = "num_incorrect_answers" - ColAllParticipants = "num_all_participants" + ColDailyQuestionTitle = "title" + ColTimeForAnswer = "time_for_answer" + ColAnswerOption = "answer_options" + ColCorrectAnswerID = "correct_answer" + ColReward = "reward" + ColStartAt = "starts_at" + ColCorrectAnswers = "num_correct_answers" + ColIncorrectAnswers = "num_incorrect_answers" + ColAllParticipants = "num_all_participants" ) type DailyQuestion struct { @@ -33,15 +40,17 @@ type DailyQuestionsQ interface { New() DailyQuestionsQ Insert(DailyQuestion) error Update(map[string]any) error - + Delete() (int64, error) Count() (int64, error) Select() ([]DailyQuestion, error) - Get() (*DailyQuestion, error) + Get() (*DailyQuestion, error) + Page(*pgdb.OffsetPageParams) DailyQuestionsQ FilterTodayQuestions(offset int) DailyQuestionsQ FilterByCreatedAtAfter(date time.Time) DailyQuestionsQ FilterByStartsAtAfter(date time.Time) DailyQuestionsQ FilterByID(ID int64) DailyQuestionsQ + FilterDayQuestions(location *time.Location, day time.Time) DailyQuestionsQ IncrementCorrectAnswer() error IncrementIncorrectAnswer() error @@ -49,13 +58,11 @@ type DailyQuestionsQ interface { } func (q *DailyQuestion) ExtractOptions() ([]resources.DailyQuestionOptions, error) { - var options struct { - Options []resources.DailyQuestionOptions `json:"options"` - } + var options []resources.DailyQuestionOptions err := json.NewDecoder(bytes.NewReader(q.AnswerOptions)).Decode(&options) if err != nil { return nil, fmt.Errorf("failed to unmarshal question options: %w", err) } - return options.Options, nil + return options, nil } diff --git a/internal/data/pg/daily_questions.go b/internal/data/pg/daily_questions.go index b82d442..8a76bd4 100644 --- a/internal/data/pg/daily_questions.go +++ b/internal/data/pg/daily_questions.go @@ -18,6 +18,7 @@ type dailyQuestionsQ struct { selector squirrel.SelectBuilder updater squirrel.UpdateBuilder counter squirrel.SelectBuilder + deleter squirrel.DeleteBuilder } func NewDailyQuestionsQ(db *pgdb.DB) data.DailyQuestionsQ { @@ -25,6 +26,7 @@ func NewDailyQuestionsQ(db *pgdb.DB) data.DailyQuestionsQ { db: db, selector: squirrel.Select("*").From(dailyQuestionsTable), updater: squirrel.Update(dailyQuestionsTable), + deleter: squirrel.Delete(dailyQuestionsTable), counter: squirrel.Select("COUNT(*) as count").From(dailyQuestionsTable), } } @@ -58,6 +60,20 @@ func (q *dailyQuestionsQ) Update(fields map[string]any) error { return nil } +func (q *dailyQuestionsQ) Delete() (int64, error) { + res, err := q.db.ExecWithResult(q.deleter) + if err != nil { + return 0, fmt.Errorf("delete daily question: %w", err) + } + + rows, err := res.RowsAffected() + if err != nil { + return 0, fmt.Errorf("count rows affected: %w", err) + } + + return rows, nil +} + func (q *dailyQuestionsQ) Count() (int64, error) { res := struct { Count int64 `db:"count"` @@ -90,6 +106,11 @@ func (q *dailyQuestionsQ) Get() (*data.DailyQuestion, error) { return &res, nil } +func (q *dailyQuestionsQ) Page(page *pgdb.OffsetPageParams) data.DailyQuestionsQ { + q.selector = page.ApplyTo(q.selector, "starts_at") + return q +} + func (q *dailyQuestionsQ) FilterByCreatedAtAfter(date time.Time) data.DailyQuestionsQ { where := fmt.Sprintf("created_at >= '%s'::timestamp with time zone", date.Format("2006-01-02 15:04:05 -0700")) q.selector = q.selector.Where(where) @@ -119,6 +140,17 @@ func (q *dailyQuestionsQ) FilterTodayQuestions(offset int) data.DailyQuestionsQ }) } +func (q *dailyQuestionsQ) FilterDayQuestions(location *time.Location, day time.Time) data.DailyQuestionsQ { + dayInLocation := day.In(location) + dayStart := time.Date(dayInLocation.Year(), dayInLocation.Month(), dayInLocation.Day(), 0, 0, 0, 0, location) + dayEnd := dayStart.Add(24 * time.Hour) + + return q.applyCondition(squirrel.And{ + squirrel.GtOrEq{"starts_at": dayStart}, + squirrel.Lt{"starts_at": dayEnd}, + }) +} + func (q *dailyQuestionsQ) FilterByID(ID int64) data.DailyQuestionsQ { return q.applyCondition(squirrel.Eq{"id": ID}) } @@ -152,6 +184,7 @@ func (q *dailyQuestionsQ) IncrementAllParticipants() error { func (q *dailyQuestionsQ) applyCondition(cond squirrel.Sqlizer) data.DailyQuestionsQ { q.selector = q.selector.Where(cond) q.updater = q.updater.Where(cond) + q.deleter = q.deleter.Where(cond) q.counter = q.counter.Where(cond) return q } diff --git a/internal/service/handlers/daily_question_create.go b/internal/service/handlers/daily_question_create.go new file mode 100644 index 0000000..3fd5d1b --- /dev/null +++ b/internal/service/handlers/daily_question_create.go @@ -0,0 +1,190 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "github.com/rarimo/geo-points-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func CreateDailyQuestion(w http.ResponseWriter, r *http.Request) { + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + req, err := requests.NewDailyQuestion(r) + if err != nil { + Log(r).WithError(err).Error("Error get request NewDailyQuestion") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + attributes := req.Data.Attributes + + if req.Data.Type != resources.DAILY_QUESTIONS { + err := fmt.Errorf("invalid request data type %s", req.Data.Type) + Log(r).WithError(err).Error("Invalid data type") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "type": fmt.Errorf("%v not allowed for this endpoint, must be %v err: %s", req.Data.Type, resources.DAILY_QUESTIONS, err), + })...) + return + } + + err = ValidateOptions(attributes.Options) + if err != nil { + Log(r).WithError(err).Error("Error Answer Options") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "options": fmt.Errorf("invalid options: %v, err: %s", attributes.Options, err), + })...) + return + } + + location := DailyQuestions(r).Location + timeReq, err := time.Parse("2006-01-02", attributes.StartsAt) + if err != nil { + Log(r).WithError(err).Error("Failed to parse start time") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "starts_at": fmt.Errorf("failed to parse start time %s err: %s", attributes.StartsAt, err), + })...) + return + } + nowTime := time.Now().UTC() + if !timeReq.After(time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day()+1, 0, 0, 0, 0, DailyQuestions(r).Location)) { + Log(r).Errorf("Arg start_at must be more or equal tomorow midnoght noe: %s", timeReq.String()) + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "starts_at": fmt.Errorf("argument start_at must be more or equal tomorow midnoght now its: %s", timeReq.String()), + })...) + return + } + + question, err := DailyQuestionsQ(r).FilterDayQuestions(location, timeReq).Get() + if err != nil { + Log(r).WithError(err).Error("Error on this day") + ape.RenderErr(w, problems.InternalError()) + return + } + if question != nil { + Log(r).Errorf("Question already exist for date %s, question: %+v", question.StartsAt, question) + ape.RenderErr(w, problems.Conflict()) + return + } + + answerOptions, err := json.Marshal(attributes.Options) + if err != nil { + Log(r).WithError(err).Error("Failed to get questions") + ape.RenderErr(w, problems.InternalError()) + return + } + + correctAnswerFound := false + for _, option := range attributes.Options { + if option.Id == int(attributes.CorrectAnswer) { + correctAnswerFound = true + break + } + } + if !correctAnswerFound { + Log(r).Errorf("Correct answer option out of range: %v", attributes.CorrectAnswer) + ape.RenderErr(w, problems.BadRequest( + validation.Errors{ + "correct_answer": fmt.Errorf("correct answer option out of range %v", attributes.CorrectAnswer), + })...) + return + } + + if attributes.Reward <= 0 { + Log(r).Errorf("Reward option out of range: %v", attributes.Reward) + ape.RenderErr(w, problems.BadRequest( + validation.Errors{ + "reward": fmt.Errorf("reward less than or equal to 0 reward: %v", attributes.Reward), + })...) + return + } + + stmt := data.DailyQuestion{ + Title: attributes.Title, + TimeForAnswer: attributes.TimeForAnswer, + Reward: attributes.Reward, + AnswerOptions: answerOptions, + CorrectAnswer: attributes.CorrectAnswer, + StartsAt: timeReq, + } + + err = DailyQuestionsQ(r).Insert(stmt) + if err != nil { + Log(r).WithError(err).Error("Error ger request NewDailyQuestion") + ape.RenderErr(w, problems.InternalError()) + return + } + + question, err = DailyQuestionsQ(r).FilterDayQuestions(location, timeReq).Get() + if err != nil { + Log(r).WithError(err).Error("Error on this day") + ape.RenderErr(w, problems.InternalError()) + return + } + if question == nil { + Log(r).Errorf("Error get question for response") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, NewDailyQuestionCreate(&stmt, attributes.Options, question.ID)) +} + +func ValidateOptions(options []resources.DailyQuestionOptions) error { + if len(options) < 2 || len(options) > 6 { + return fmt.Errorf("the number of options must be between 2 and 6") + } + + uniqueTitles := make(map[string]bool) + + for _, option := range options { + if option.Title == "" { + return fmt.Errorf("option titles must not be empty") + } + if _, exists := uniqueTitles[option.Title]; exists { + return fmt.Errorf("option titles must be unique, found duplicate: %s", option.Title) + } + uniqueTitles[option.Title] = true + } + + ids := make([]int, len(options)) + for i, option := range options { + ids[i] = option.Id + } + sort.Ints(ids) + for i := 0; i < len(ids); i++ { + if ids[i] != i { + return fmt.Errorf("option IDs must be sequential and start from 0") + } + } + return nil +} + +func NewDailyQuestionCreate(q *data.DailyQuestion, options []resources.DailyQuestionOptions, ID int64) resources.DailyQuestionDetailsResponse { + return resources.DailyQuestionDetailsResponse{ + Data: resources.DailyQuestionDetails{ + Key: resources.NewKeyInt64(ID, resources.DAILY_QUESTIONS), + Attributes: resources.DailyQuestionDetailsAttributes{ + Title: q.Title, + Options: options, + CorrectAnswer: q.CorrectAnswer, + Reward: q.Reward, + TimeForAnswer: q.TimeForAnswer, + StartsAt: q.StartsAt.String(), + CreatedAt: time.Now().UTC().String(), + }, + }, + } + +} diff --git a/internal/service/handlers/daily_question_delete.go b/internal/service/handlers/daily_question_delete.go new file mode 100644 index 0000000..8c0ad09 --- /dev/null +++ b/internal/service/handlers/daily_question_delete.go @@ -0,0 +1,93 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-chi/chi" + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func DeleteDailyQuestion(w http.ResponseWriter, r *http.Request) { + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + IDStr := strings.ToLower(chi.URLParam(r, "question_id")) + ID, err := strconv.ParseInt(IDStr, 10, 64) + Log(r).Infof("ID: %v", ID) + if err != nil { + Log(r).WithError(err).Error("failed to parse ID") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + question, err := DailyQuestionsQ(r).FilterByID(ID).Get() + if err != nil { + Log(r).WithError(err).Error("Error getting question") + ape.RenderErr(w, problems.InternalError()) + return + } + if question == nil { + Log(r).Errorf("Question with ID %d not found", ID) + ape.RenderErr(w, problems.NotFound()) + return + } + deletedQuestion := *question + + timeReq := question.StartsAt + nowTime := time.Now().UTC() + if !timeReq.After(time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day()+1, 0, 0, 0, 0, DailyQuestions(r).Location)) { + Log(r).Errorf("Only questions that start tomorrow or later can be delete: %s", timeReq.String()) + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + _, err = DailyQuestionsQ(r).New().FilterByID(ID).Delete() + if err != nil { + Log(r).WithError(err).Error("Error deleting daily question") + ape.RenderErr(w, problems.InternalError()) + return + } + + response, err := NewDailyQuestionDelete(ID, deletedQuestion) + if err != nil { + Log(r).WithError(err).Error("Error deleting daily question") + ape.RenderErr(w, problems.InternalError()) + return + } + ape.Render(w, response) +} + +func NewDailyQuestionDelete(ID int64, q data.DailyQuestion) (resources.DailyQuestionDetailsResponse, error) { + var options []resources.DailyQuestionOptions + err := json.Unmarshal(q.AnswerOptions, &options) + if err != nil { + err = fmt.Errorf("failed to unmarshal AnswerOptions: %v", err) + return resources.DailyQuestionDetailsResponse{}, err + } + + return resources.DailyQuestionDetailsResponse{ + Data: resources.DailyQuestionDetails{ + Key: resources.NewKeyInt64(ID, resources.DAILY_QUESTIONS), + Attributes: resources.DailyQuestionDetailsAttributes{ + Title: q.Title, + Options: options, + CorrectAnswer: q.CorrectAnswer, + Reward: q.Reward, + TimeForAnswer: q.TimeForAnswer, + StartsAt: q.StartsAt.String(), + CreatedAt: q.CreatedAt.String(), + }, + }, + }, nil +} diff --git a/internal/service/handlers/daily_question_edit.go b/internal/service/handlers/daily_question_edit.go new file mode 100644 index 0000000..19380b6 --- /dev/null +++ b/internal/service/handlers/daily_question_edit.go @@ -0,0 +1,237 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "github.com/rarimo/geo-points-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func EditDailyQuestion(w http.ResponseWriter, r *http.Request) { + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + IDStr := strings.ToLower(chi.URLParam(r, "question_id")) + ID, err := strconv.ParseInt(IDStr, 10, 64) + if err != nil { + Log(r).WithError(err).Error("Failed to parse ID") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "query": fmt.Errorf("failed to parse ID: %v, err: %s", ID, err), + })...) + return + } + + req, err := requests.NewDailyQuestionEdit(r) + if err != nil { + Log(r).WithError(err).Error("Error creating daily question edit request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + attributes := req.Data.Attributes + if req.Data.Type != resources.DAILY_QUESTIONS { + err := fmt.Errorf("invalid request data type %s", req.Data.Type) + Log(r).WithError(err).Error("Invalid data type") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "type": fmt.Errorf("%v not allowed for this endpoint, must be %v err: %s", req.Data.Type, resources.DAILY_QUESTIONS, err), + })...) + return + } + + question, err := DailyQuestionsQ(r).FilterByID(ID).Get() + if err != nil { + Log(r).WithError(err).Error("Error getting question") + ape.RenderErr(w, problems.InternalError()) + return + } + if question == nil { + Log(r).Errorf("Question with ID %d not found", ID) + ape.RenderErr(w, problems.NotFound()) + return + } + + nowTime := time.Now().UTC() + if !question.StartsAt.After(time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day()+1, 0, 0, 0, 0, DailyQuestions(r).Location)) { + Log(r).Errorf("Cannot change a question id: %v that is available today or in the past", ID) + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "starts_at": fmt.Errorf("cannot change a question id: %v that is available today or in the past", ID), + })...) + return + } + + requestBody := map[string]any{} + + if attributes.Title != nil { + requestBody[data.ColDailyQuestionTitle] = *attributes.Title + } + + if attributes.StartsAt != nil { + timeReq, err := time.Parse("2006-01-02", *attributes.StartsAt) + if err != nil { + Log(r).WithError(err).Error("Failed to parse start time") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "starts_at": fmt.Errorf("failed to parse start time %s err: %s", *attributes.StartsAt, err), + })...) + return + } + nowTime := time.Now().UTC() + if !timeReq.After(time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day()+1, 0, 0, 0, 0, DailyQuestions(r).Location)) { + Log(r).Errorf("Argument start_at must be more or equal tomorow midnoght now its: %s", timeReq.String()) + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "starts_at": fmt.Errorf("argument start_at must be more or equal tomorow midnoght now its: %s", timeReq.String()), + })...) + return + } + + location := DailyQuestions(r).Location + question, err := DailyQuestionsQ(r).FilterDayQuestions(location, timeReq).Get() + if err != nil { + Log(r).WithError(err).Error("Error on this day") + ape.RenderErr(w, problems.InternalError()) + return + } + if question != nil && ID != question.ID { + Log(r).Errorf("Error on this day %s, the daily question already has %s", question.StartsAt.String(), question) + ape.RenderErr(w, problems.Conflict()) + return + } + requestBody[data.ColStartAt] = attributes.StartsAt + } + + if attributes.CorrectAnswer != nil { + l := len(question.AnswerOptions) + if *attributes.CorrectAnswer < 0 || l <= int(*attributes.CorrectAnswer) { + Log(r).Error("Invalid CorrectAnswer") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "correct_answer": fmt.Errorf("invalid value for correct_answer: %v", *attributes.CorrectAnswer), + })...) + return + } + requestBody[data.ColCorrectAnswerID] = *attributes.CorrectAnswer + } + + if attributes.Options != nil { + err = ValidateOptions(*attributes.Options) + if err != nil { + Log(r).WithError(err).Error("Error Answer Options") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "options": fmt.Errorf("invalid options: %v, err: %s", *attributes.Options, err), + })...) + return + } + + answerOptions, err := json.Marshal(attributes.Options) + if err != nil { + Log(r).Errorf("Error marshalling answer options: %v", err) + ape.RenderErr(w, problems.InternalError()) + return + } + correctAnswerFound := false + + var localCorrectAnswer int64 + if attributes.CorrectAnswer != nil { + localCorrectAnswer = *attributes.CorrectAnswer + } + + for _, option := range *attributes.Options { + if option.Id == int(localCorrectAnswer) { + correctAnswerFound = true + break + } + } + if !correctAnswerFound { + Log(r).Warnf("Correct answer option out of range: %v", question.CorrectAnswer) + ape.RenderErr(w, problems.BadRequest( + validation.Errors{ + "correct_answer": fmt.Errorf("correct answer option out of range %v", localCorrectAnswer), + })...) + return + } + requestBody[data.ColAnswerOption] = answerOptions + } + + if attributes.Reward != nil { + if *attributes.Reward <= 0 { + Log(r).Error("Invalid Reward") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "reward": fmt.Errorf("reward less than or equal to 0 reward: %v", attributes.Reward), + })...) + return + } + requestBody[data.ColReward] = *attributes.Reward + } + + if attributes.TimeForAnswer != nil { + if *attributes.TimeForAnswer < 0 { + Log(r).Error("Invalid Time for answer") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "time_for_answer": fmt.Errorf("invalid value for time_for_answer: %v", *attributes.TimeForAnswer), + })...) + return + } + requestBody[data.ColTimeForAnswer] = *attributes.TimeForAnswer + } + + err = DailyQuestionsQ(r).FilterByID(ID).Update(requestBody) + if err != nil { + Log(r).WithError(err).Error("Error editing daily question") + ape.RenderErr(w, problems.InternalError()) + return + } + + questionNew, err := DailyQuestionsQ(r).FilterByID(ID).Get() + if err != nil { + Log(r).WithError(err).Error("Error on this day") + ape.RenderErr(w, problems.InternalError()) + return + } + if questionNew == nil { + Log(r).Errorf("Error get question for response") + ape.RenderErr(w, problems.InternalError()) + return + } + + resp, err := NewDailyQuestionEdite(ID, questionNew) + if err != nil { + Log(r).WithError(err).Error("Error editing daily question") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, resp) +} + +func NewDailyQuestionEdite(ID int64, q *data.DailyQuestion) (resources.DailyQuestionDetailsResponse, error) { + var options []resources.DailyQuestionOptions + err := json.Unmarshal(q.AnswerOptions, &options) + if err != nil { + err = fmt.Errorf("failed to unmarshal AnswerOptions: %v", err) + return resources.DailyQuestionDetailsResponse{}, err + } + + return resources.DailyQuestionDetailsResponse{ + Data: resources.DailyQuestionDetails{ + Key: resources.NewKeyInt64(ID, resources.DAILY_QUESTIONS), + Attributes: resources.DailyQuestionDetailsAttributes{ + CorrectAnswer: q.CorrectAnswer, + Options: options, + Reward: q.Reward, + StartsAt: q.StartsAt.String(), + TimeForAnswer: q.TimeForAnswer, + Title: q.Title, + }, + }, + }, nil +} diff --git a/internal/service/handlers/daily_questions_select.go b/internal/service/handlers/daily_questions_select.go new file mode 100644 index 0000000..f11a009 --- /dev/null +++ b/internal/service/handlers/daily_questions_select.go @@ -0,0 +1,94 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "github.com/rarimo/geo-points-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func FilterStartAtDailyQuestions(w http.ResponseWriter, r *http.Request) { + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + req, err := requests.NewFilterStartAtDailyQuestions(r) + if err != nil { + Log(r).WithError(err).Error("error creating filter start at daily questions request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + res, err := DailyQuestionsQ(r).Page(&req.OffsetPageParams).Select() + if err != nil { + Log(r).WithError(err).Error("Error filtering questions") + ape.RenderErr(w, problems.InternalError()) + return + } + + resp, err := NewDailyQuestionsFilterDate(res) + if err != nil { + Log(r).WithError(err).Error("Error filtering questions") + ape.RenderErr(w, problems.InternalError()) + return + } + questionListCount, err := DailyQuestionsQ(r).Count() + if err != nil { + Log(r).WithError(err).Error("Failed to count balances") + ape.RenderErr(w, problems.InternalError()) + return + } + resp.Links = req.GetLinks(r, uint64(questionListCount)) + if req.Count { + _ = resp.PutMeta(struct { + QuestionCount int64 `json:"question_count"` + }{questionListCount}) + } + ape.Render(w, resp) +} + +func NewDailyQuestionModel(question data.DailyQuestion) (resources.DailyQuestionDetails, error) { + var options []resources.DailyQuestionOptions + + err := json.Unmarshal(question.AnswerOptions, &options) + if err != nil { + err = fmt.Errorf("failed to unmarshal AnswerOptions: %v", err) + return resources.DailyQuestionDetails{}, err + } + + return resources.DailyQuestionDetails{ + Key: resources.NewKeyInt64(question.ID, resources.DAILY_QUESTIONS), + Attributes: resources.DailyQuestionDetailsAttributes{ + CorrectAnswer: question.CorrectAnswer, + CreatedAt: question.CreatedAt.String(), + NumAllParticipants: question.NumAllParticipants, + NumCorrectAnswers: question.NumCorrectAnswers, + NumIncorrectAnswers: question.NumIncorrectAnswers, + Options: options, + Reward: question.Reward, + StartsAt: question.StartsAt.String(), + TimeForAnswer: question.TimeForAnswer, + Title: question.Title, + }, + }, nil +} + +func NewDailyQuestionsFilterDate(questions []data.DailyQuestion) (resources.DailyQuestionDetailsListResponse, error) { + list := make([]resources.DailyQuestionDetails, len(questions)) + for i, q := range questions { + qModel, err := NewDailyQuestionModel(q) + if err != nil { + return resources.DailyQuestionDetailsListResponse{}, fmt.Errorf("error make %s daily question model, %s", q, err) + } + list[i] = qModel + } + + return resources.DailyQuestionDetailsListResponse{Data: list}, nil +} diff --git a/internal/service/requests/daily_question_create.go b/internal/service/requests/daily_question_create.go new file mode 100644 index 0000000..7ddf96d --- /dev/null +++ b/internal/service/requests/daily_question_create.go @@ -0,0 +1,22 @@ +package requests + +import ( + "encoding/json" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewDailyQuestion(r *http.Request) (req resources.DailyQuestionCreateResponse, err error) { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + return req, validation.Errors{ + "data/id": validation.Validate(&req.Data.ID), + "data/type": validation.Validate(&req.Data.Type, validation.Required), + "data/attributes": validation.Validate(&req.Data.Attributes, validation.Required), + }.Filter() +} diff --git a/internal/service/requests/daily_question_edit.go b/internal/service/requests/daily_question_edit.go new file mode 100644 index 0000000..9017b4d --- /dev/null +++ b/internal/service/requests/daily_question_edit.go @@ -0,0 +1,22 @@ +package requests + +import ( + "encoding/json" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewDailyQuestionEdit(r *http.Request) (req resources.DailyQuestionEditResponse, err error) { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + return req, validation.Errors{ + "data/id": validation.Validate(&req.Data.ID), + "data/type": validation.Validate(&req.Data.Type, validation.Required), + "data/attributes": validation.Validate(&req.Data.Attributes, validation.Required), + }.Filter() +} diff --git a/internal/service/requests/daily_question_filter.go b/internal/service/requests/daily_question_filter.go new file mode 100644 index 0000000..3602bda --- /dev/null +++ b/internal/service/requests/daily_question_filter.go @@ -0,0 +1,22 @@ +package requests + +import ( + "net/http" + + "github.com/rarimo/geo-points-svc/internal/service/page" + "gitlab.com/distributed_lab/urlval/v4" +) + +type QuestionList struct { + page.OffsetParams + Count bool `url:"count"` +} + +func NewFilterStartAtDailyQuestions(r *http.Request) (req QuestionList, err error) { + if err = urlval.Decode(r.URL.Query(), &req); err != nil { + err = newDecodeError("query", err) + return + } + + return req, req.Validate() +} diff --git a/internal/service/router.go b/internal/service/router.go index 6b49672..302a1a6 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -45,11 +45,20 @@ func Run(ctx context.Context, cfg config.Config) { }) }) }) - r.Route("/daily_questions/{nullifier}", func(r chi.Router) { + + r.Route("/daily_questions", func(r chi.Router) { r.Use(authMW) - r.Get("/status", handlers.GetDailyQuestionsStatus) - r.Get("/", handlers.GetDailyQuestion) - r.Post("/", handlers.CheckDailyQuestion) + r.Route("/admin", func(r chi.Router) { + r.Delete("/{question_id}", handlers.DeleteDailyQuestion) + r.Patch("/{question_id}", handlers.EditDailyQuestion) + r.Post("/", handlers.CreateDailyQuestion) + r.Get("/", handlers.FilterStartAtDailyQuestions) + }) + r.Route("/{nullifier}", func(r chi.Router) { + r.Get("/status", handlers.GetDailyQuestionsStatus) + r.Get("/", handlers.GetDailyQuestion) + r.Post("/", handlers.CheckDailyQuestion) + }) }) r.Route("/events", func(r chi.Router) { r.Use(authMW) diff --git a/resources/model_daily_question_create.go b/resources/model_daily_question_create.go new file mode 100644 index 0000000..32cce87 --- /dev/null +++ b/resources/model_daily_question_create.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type DailyQuestionCreate struct { + Key + Attributes DailyQuestionCreateAttributes `json:"attributes"` +} +type DailyQuestionCreateResponse struct { + Data DailyQuestionCreate `json:"data"` + Included Included `json:"included"` +} + +type DailyQuestionCreateListResponse struct { + Data []DailyQuestionCreate `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *DailyQuestionCreateListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *DailyQuestionCreateListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustDailyQuestionCreate - returns DailyQuestionCreate from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustDailyQuestionCreate(key Key) *DailyQuestionCreate { + var dailyQuestionCreate DailyQuestionCreate + if c.tryFindEntry(key, &dailyQuestionCreate) { + return &dailyQuestionCreate + } + return nil +} diff --git a/resources/model_daily_question_create_attributes.go b/resources/model_daily_question_create_attributes.go new file mode 100644 index 0000000..fbdb8db --- /dev/null +++ b/resources/model_daily_question_create_attributes.go @@ -0,0 +1,20 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type DailyQuestionCreateAttributes struct { + // Correct answer ID + CorrectAnswer int64 `json:"correct_answer"` + // Answer options. Minimum 2, maximum 6 + Options []DailyQuestionOptions `json:"options"` + // Reward for a correct answer + Reward int64 `json:"reward"` + // Start date when this question is available, hours and minutes are always 0 + StartsAt string `json:"starts_at"` + // Time for answer + TimeForAnswer int64 `json:"time_for_answer"` + // Question title + Title string `json:"title"` +} diff --git a/resources/model_daily_question_del.go b/resources/model_daily_question_del.go new file mode 100644 index 0000000..90542d3 --- /dev/null +++ b/resources/model_daily_question_del.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type DailyQuestionDel struct { + Key + Attributes DailyQuestionDelAttributes `json:"attributes"` +} +type DailyQuestionDelResponse struct { + Data DailyQuestionDel `json:"data"` + Included Included `json:"included"` +} + +type DailyQuestionDelListResponse struct { + Data []DailyQuestionDel `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *DailyQuestionDelListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *DailyQuestionDelListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustDailyQuestionDel - returns DailyQuestionDel from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustDailyQuestionDel(key Key) *DailyQuestionDel { + var dailyQuestionDel DailyQuestionDel + if c.tryFindEntry(key, &dailyQuestionDel) { + return &dailyQuestionDel + } + return nil +} diff --git a/resources/model_daily_question_del_attributes.go b/resources/model_daily_question_del_attributes.go new file mode 100644 index 0000000..3d50eed --- /dev/null +++ b/resources/model_daily_question_del_attributes.go @@ -0,0 +1,10 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type DailyQuestionDelAttributes struct { + // Question title + Title string `json:"title"` +} diff --git a/resources/model_daily_question_details.go b/resources/model_daily_question_details.go new file mode 100644 index 0000000..11f9d7b --- /dev/null +++ b/resources/model_daily_question_details.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type DailyQuestionDetails struct { + Key + Attributes DailyQuestionDetailsAttributes `json:"attributes"` +} +type DailyQuestionDetailsResponse struct { + Data DailyQuestionDetails `json:"data"` + Included Included `json:"included"` +} + +type DailyQuestionDetailsListResponse struct { + Data []DailyQuestionDetails `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *DailyQuestionDetailsListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *DailyQuestionDetailsListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustDailyQuestionDetails - returns DailyQuestionDetails from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustDailyQuestionDetails(key Key) *DailyQuestionDetails { + var dailyQuestionDetails DailyQuestionDetails + if c.tryFindEntry(key, &dailyQuestionDetails) { + return &dailyQuestionDetails + } + return nil +} diff --git a/resources/model_daily_question_details_attributes.go b/resources/model_daily_question_details_attributes.go new file mode 100644 index 0000000..52b635a --- /dev/null +++ b/resources/model_daily_question_details_attributes.go @@ -0,0 +1,28 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type DailyQuestionDetailsAttributes struct { + // Correct answer ID + CorrectAnswer int64 `json:"correct_answer"` + // Start date when this question was create + CreatedAt string `json:"created_at"` + // Users who received the question, those who answered and those who did not answer in the time given to them + NumAllParticipants int64 `json:"num_all_participants"` + // Number of correct answers + NumCorrectAnswers int64 `json:"num_correct_answers"` + // Number of incorrect answers + NumIncorrectAnswers int64 `json:"num_incorrect_answers"` + // Answer options. Minimum 2, maximum 6 + Options []DailyQuestionOptions `json:"options"` + // Reward for a correct answer + Reward int64 `json:"reward"` + // Start date when this question is available, hours and minutes are always 0 + StartsAt string `json:"starts_at"` + // Time for answer + TimeForAnswer int64 `json:"time_for_answer"` + // Question title + Title string `json:"title"` +} diff --git a/resources/model_daily_question_edit.go b/resources/model_daily_question_edit.go new file mode 100644 index 0000000..8ef533a --- /dev/null +++ b/resources/model_daily_question_edit.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type DailyQuestionEdit struct { + Key + Attributes DailyQuestionEditAttributes `json:"attributes"` +} +type DailyQuestionEditResponse struct { + Data DailyQuestionEdit `json:"data"` + Included Included `json:"included"` +} + +type DailyQuestionEditListResponse struct { + Data []DailyQuestionEdit `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *DailyQuestionEditListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *DailyQuestionEditListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustDailyQuestionEdit - returns DailyQuestionEdit from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustDailyQuestionEdit(key Key) *DailyQuestionEdit { + var dailyQuestionEdit DailyQuestionEdit + if c.tryFindEntry(key, &dailyQuestionEdit) { + return &dailyQuestionEdit + } + return nil +} diff --git a/resources/model_daily_question_edit_attributes.go b/resources/model_daily_question_edit_attributes.go new file mode 100644 index 0000000..2dfd737 --- /dev/null +++ b/resources/model_daily_question_edit_attributes.go @@ -0,0 +1,20 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type DailyQuestionEditAttributes struct { + // Correct answer ID + CorrectAnswer *int64 `json:"correct_answer,omitempty"` + // Answer options. Minimum 2, maximum 6 + Options *[]DailyQuestionOptions `json:"options,omitempty"` + // Reward for a correct answer + Reward *int64 `json:"reward,omitempty"` + // Start date when this question is available, hours and minutes are always 0 + StartsAt *string `json:"starts_at,omitempty"` + // Time for answer + TimeForAnswer *int64 `json:"time_for_answer,omitempty"` + // Question title + Title *string `json:"title,omitempty"` +}