From 321da1e6c5b1fe0b4312817f7e5fcf90bb06a788 Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Wed, 22 Dec 2021 21:38:22 +0700 Subject: [PATCH] feat: add quest service --- .gitignore | 2 +- api/api.go | 50 +-- api/auth.go | 2 +- api/interceptors.go | 4 +- api/quest.go | 95 +++++ api/user.go | 35 +- api/v1/all.pb.go | 443 ++++++++++----------- api/v1/codequest.gunk | 2 - api/v1/gen/json/codequest/all.swagger.json | 11 +- api/v1/gen/ts-gateway/codequest/all.pb.ts | 4 +- api/v1/gen/ts/codequest/all_pb.d.ts | 14 +- api/v1/quest.gunk | 11 +- cmd/srv/main.go | 9 +- db/quest.go | 119 ++++++ gen.sh | 67 ++++ go.mod | 7 +- go.sum | 31 +- models/allpartsdata.xo.go | 63 +++ models/partsdata.xo.go | 47 +++ models/questionattempt.xo.go | 165 ++++++++ models/submissions.xo.go | 38 ++ models/user.xo.go | 53 ++- models/userinfo.xo.go | 152 +++++++ models/xo.xo.yaml | 108 ++++- question/data.go | 19 + question/q01/q01.go | 9 +- question/q01/q01.md | 11 +- question/q02/q02.go | 9 +- question/q02/q02.md | 11 +- question/q03.go | 1 - question/question.go | 21 +- sql/schema.sql | 24 +- 32 files changed, 1254 insertions(+), 383 deletions(-) create mode 100644 api/quest.go create mode 100644 db/quest.go create mode 100644 models/allpartsdata.xo.go create mode 100644 models/partsdata.xo.go create mode 100644 models/questionattempt.xo.go create mode 100644 models/submissions.xo.go create mode 100644 models/userinfo.xo.go create mode 100644 question/data.go delete mode 100644 question/q03.go diff --git a/.gitignore b/.gitignore index 73f72c7..ae24a1d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ node_modules client.secret.json .vim -*.sqlite +*.sqlite* diff --git a/api/api.go b/api/api.go index 9e28138..a408e7f 100644 --- a/api/api.go +++ b/api/api.go @@ -5,9 +5,9 @@ import ( codequestpb "github.com/hhhapz/codequest/api/v1" "github.com/hhhapz/codequest/models" + "github.com/hhhapz/codequest/question" "golang.org/x/oauth2" "google.golang.org/grpc" - "google.golang.org/grpc/codes" "google.golang.org/grpc/reflection" ) @@ -16,32 +16,40 @@ import ( var inProd bool type OAuthStore interface { - Create(callback string) (code string) - Validate(ctx context.Context, state, code string) (*oauth2.Token, string, error) + Create(string) string + Validate(context.Context, string, string) (*oauth2.Token, string, error) } type UserStore interface { - ConsumeToken(ctx context.Context, token *oauth2.Token) (*models.Token, error) + ConsumeToken(context.Context, *oauth2.Token) (*models.Token, error) - CreateToken(ctx context.Context, user *models.User) (*models.Token, error) - RevokeToken(ctx context.Context, token string) error - RevokeUserTokens(ctx context.Context, user *models.User) (int64, error) + CreateToken(context.Context, *models.User) (*models.Token, error) + RevokeToken(context.Context, string) error + RevokeUserTokens(context.Context, *models.User) (int64, error) - User(ctx context.Context, email string) (*models.User, error) - Users(ctx context.Context) ([]*models.User, error) + User(context.Context, string) (*models.User, error) + Users(context.Context) ([]*models.User, error) - UserByToken(ctx context.Context, token string) (*models.User, error) + UserByToken(context.Context, string) (*models.User, error) - UpdateUser(ctx context.Context, user *models.User) error - DeleteUser(ctx context.Context, user *models.User) error + UpdateUser(context.Context, *models.User) error + DeleteUser(context.Context, *models.User) error +} + +type QuestStore interface { + Questions(context.Context, *models.User) ([]*question.Data, error) + Question(context.Context, *models.User, string) (*question.Data, error) + Submissions(context.Context, *models.User, string, question.Part) (bool, int, error) + AddSubmission(context.Context, *models.QuestionAttempt) error } type Server struct { *AuthService *UserService + *QuestService } -func NewServer(os OAuthStore, us UserStore) (*grpc.Server, error) { +func NewServer(os OAuthStore, us UserStore, qs QuestStore) (*grpc.Server, error) { s := &Server{ AuthService: &AuthService{ oauthStore: os, @@ -50,6 +58,10 @@ func NewServer(os OAuthStore, us UserStore) (*grpc.Server, error) { UserService: &UserService{ userStore: us, }, + QuestService: &QuestService{ + userStore: us, + questStore: qs, + }, } srv := grpc.NewServer( grpc.UnaryInterceptor(AuthInterceptor(s.AuthService.defaultAuthFunc)), @@ -57,16 +69,6 @@ func NewServer(os OAuthStore, us UserStore) (*grpc.Server, error) { reflection.Register(srv) codequestpb.RegisterAuthServiceServer(srv, s.AuthService) codequestpb.RegisterUserServiceServer(srv, s.UserService) + codequestpb.RegisterQuestServiceServer(srv, s.QuestService) return srv, nil } - -// List of commonly used error values -var ( - ErrInvalidArgument = grpc.Errorf( - codes.InvalidArgument, - "Invalid argument", - ) -) - -func err() { -} diff --git a/api/auth.go b/api/auth.go index 47e5b71..4128361 100644 --- a/api/auth.go +++ b/api/auth.go @@ -53,7 +53,7 @@ func (as *AuthService) Token(ctx context.Context, req *codequestpb.TokenRequest) return nil, status.Errorf(codes.InvalidArgument, err.Error()) } log.Printf("could not perform token exchange: %v", err) - return nil, status.Errorf(codes.InvalidArgument, "invalid code %q: %v", req.Code, err) + return nil, status.Errorf(codes.Internal, "could not sign in") } tk, err := as.userStore.ConsumeToken(ctx, token) diff --git a/api/interceptors.go b/api/interceptors.go index 3ad4613..7ed261b 100644 --- a/api/interceptors.go +++ b/api/interceptors.go @@ -13,8 +13,8 @@ import ( ) const ( - UserKey = "ctxuser" - TokenKey = "ctxtoken" + UserKey = "ctxUser" + TokenKey = "ctxToken" ) func AuthInterceptor(authFunc grpc_auth.AuthFunc) grpc.UnaryServerInterceptor { diff --git a/api/quest.go b/api/quest.go new file mode 100644 index 0000000..fa8dfb5 --- /dev/null +++ b/api/quest.go @@ -0,0 +1,95 @@ +package api + +import ( + "bytes" + "context" + "errors" + "log" + + codequestpb "github.com/hhhapz/codequest/api/v1" + "github.com/hhhapz/codequest/models" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +type QuestService struct { + codequestpb.UnimplementedQuestServiceServer + + questStore QuestStore + userStore UserStore +} + +func (qs *QuestService) Questions(ctx context.Context, req *emptypb.Empty) (*codequestpb.QuestionsResponse, error) { + u := UserCtx(ctx) + + questions, err := qs.questStore.Questions(ctx, u) + if err != nil { + if errors.As(err, &models.UserError{}) { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return nil, status.Errorf(codes.Internal, "could not get questions: internal error") + } + + res := new(codequestpb.QuestionsResponse) + + for _, q := range questions { + res.Questions = append(res.Questions, &codequestpb.Question{ + ID: q.QuestionID, + Title: q.Name, + Part1: &codequestpb.PartData{ + Completed: q.Part1.Completed, + PointsWorth: int32(q.Part1.PointsWorth), + }, + Part2: &codequestpb.PartData{ + Completed: q.Part2.Completed, + PointsWorth: int32(q.Part2.PointsWorth), + }, + }) + } + + return res, nil +} + +func (qs *QuestService) QuestionByID(ctx context.Context, req *codequestpb.QuestionByIDRequest) (*codequestpb.Question, error) { + u := UserCtx(ctx) + + question, err := qs.questStore.Question(ctx, u, req.ID) + if err != nil { + if errors.As(err, &models.UserError{}) { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return nil, status.Errorf(codes.Internal, "could not get question: internal error") + } + + content := new(bytes.Buffer) + err = question.Text.Execute(content, question) + if err != nil { + log.Printf("could gen write question %s template: %v", req.ID, err) + return nil, status.Errorf(codes.Internal, "could not get question: internal error") + } + + q := &codequestpb.Question{ + ID: question.QuestionID, + Title: question.Name, + Text: content.String(), + Part1: &codequestpb.PartData{ + Completed: question.Part1.Completed, + PointsWorth: int32(question.Part1.PointsWorth), + }, + Part2: &codequestpb.PartData{ + Completed: question.Part2.Completed, + PointsWorth: int32(question.Part2.PointsWorth), + }, + } + + return q, nil +} + +func (qs *QuestService) QuestionInput(ctx context.Context, req *codequestpb.QuestionInputRequest) (*codequestpb.QuestionInput, error) { + panic("not implemented") // TODO: Implement +} + +func (qs *QuestService) Submit(ctx context.Context, req *codequestpb.SubmitRequest) (*codequestpb.SubmitResponse, error) { + panic("not implemented") // TODO: Implement +} diff --git a/api/user.go b/api/user.go index 0398a6d..9cc53ef 100644 --- a/api/user.go +++ b/api/user.go @@ -2,7 +2,6 @@ package api import ( "context" - "database/sql" "log" codequestpb "github.com/hhhapz/codequest/api/v1" @@ -70,11 +69,12 @@ func (us *UserService) UpdateUser(ctx context.Context, req *codequestpb.UpdateUs return nil, status.Errorf(codes.InvalidArgument, "name must be between 3 and 20 characters") } - gl := req.Body.GradeLevel - u.GradeLevel = sql.NullInt64{Int64: int64(gl), Valid: true} - if gl < 9 || gl > 12 { - return nil, status.Errorf(codes.InvalidArgument, "grade level must be between 9 and 12") - } + // TODO FIX THIS + // gl := req.Body.GradeLevel + // u.GradeLevel = sql.NullInt64{Int64: int64(gl), Valid: true} + // if gl < 9 || gl > 12 { + // return nil, status.Errorf(codes.InvalidArgument, "grade level must be between 9 and 12") + // } if err := us.userStore.UpdateUser(ctx, u); err != nil { log.Printf("could not update user: %v", err) @@ -101,11 +101,11 @@ func (us *UserService) AdminUpdateUser(ctx context.Context, req *codequestpb.Upd return nil, status.Errorf(codes.InvalidArgument, "name must be between 3 and 20 characters") } - gl := req.Body.GradeLevel - user.GradeLevel = sql.NullInt64{Int64: int64(gl), Valid: true} - if gl < 9 || gl > 12 { - return nil, status.Errorf(codes.InvalidArgument, "grade level must be between 9 and 12") - } + // gl := req.Body.GradeLevel + // user.GradeLevel = sql.NullInt64{Int64: int64(gl), Valid: true} + // if gl < 9 || gl > 12 { + // return nil, status.Errorf(codes.InvalidArgument, "grade level must be between 9 and 12") + // } user.Admin = req.Body.Admin @@ -139,12 +139,11 @@ func (us *UserService) DeleteUser(ctx context.Context, req *codequestpb.DeleteUs func convertUser(u *models.User) *codequestpb.User { return &codequestpb.User{ - Admin: u.Admin, - Email: u.Email, - GradeLevel: int32(u.GradeLevel.Int64), - ID: u.ID, - Name: u.Name, - Picture: u.Picture, - CreatedAt: timestamppb.New(u.CreatedAt.Time()), + ID: u.ID, + Name: u.Name, + Email: u.Email, + Picture: u.Picture, + Admin: u.Admin, + CreatedAt: timestamppb.New(u.CreatedAt.Time()), } } diff --git a/api/v1/all.pb.go b/api/v1/all.pb.go index 3cebe36..2c1905a 100644 --- a/api/v1/all.pb.go +++ b/api/v1/all.pb.go @@ -287,8 +287,6 @@ type User struct { Email string `protobuf:"bytes,3,opt,name=Email,json=email,proto3" json:"email,omitempty"` // Picture is the URL of the user's profile picture. Picture string `protobuf:"bytes,4,opt,name=Picture,json=picture,proto3" json:"picture,omitempty"` - // GradeLevel of the user. - GradeLevel int32 `protobuf:"varint,5,opt,name=GradeLevel,json=grade_level,proto3" json:"grade_level,omitempty"` // Admin is true if the user is an administrator. Admin bool `protobuf:"varint,6,opt,name=Admin,json=admin,proto3" json:"admin,omitempty"` // CreatedAt is the time the user was created. @@ -355,13 +353,6 @@ func (x *User) GetPicture() string { return "" } -func (x *User) GetGradeLevel() int32 { - if x != nil { - return x.GradeLevel - } - return 0 -} - func (x *User) GetAdmin() bool { if x != nil { return x.Admin @@ -381,7 +372,6 @@ type PartData struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Part int32 `protobuf:"varint,1,opt,name=Part,json=part,proto3" json:"part,omitempty"` Completed bool `protobuf:"varint,2,opt,name=Completed,json=completed,proto3" json:"completed,omitempty"` PointsWorth int32 `protobuf:"varint,3,opt,name=PointsWorth,json=points_worth,proto3" json:"points_worth,omitempty"` } @@ -418,13 +408,6 @@ func (*PartData) Descriptor() ([]byte, []int) { return file_github_com_hhhapz_codequest_api_v1_all_proto_rawDescGZIP(), []int{6} } -func (x *PartData) GetPart() int32 { - if x != nil { - return x.Part - } - return 0 -} - func (x *PartData) GetCompleted() bool { if x != nil { return x.Completed @@ -444,11 +427,11 @@ type Question struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ID string `protobuf:"bytes,1,opt,name=ID,json=id,proto3" json:"id,omitempty"` - Title string `protobuf:"bytes,2,opt,name=Title,json=title,proto3" json:"title,omitempty"` - Content string `protobuf:"bytes,3,opt,name=Content,json=content,proto3" json:"content,omitempty"` - Part1 *PartData `protobuf:"bytes,4,opt,name=Part1,json=part1,proto3" json:"part1,omitempty"` - Part2 *PartData `protobuf:"bytes,5,opt,name=Part2,json=part2,proto3" json:"part2,omitempty"` + ID string `protobuf:"bytes,1,opt,name=ID,json=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=Title,json=title,proto3" json:"title,omitempty"` + Text string `protobuf:"bytes,3,opt,name=Text,json=text,proto3" json:"text,omitempty"` + Part1 *PartData `protobuf:"bytes,4,opt,name=Part1,json=part1,proto3" json:"part1,omitempty"` + Part2 *PartData `protobuf:"bytes,5,opt,name=Part2,json=part2,proto3" json:"part2,omitempty"` } func (x *Question) Reset() { @@ -497,9 +480,9 @@ func (x *Question) GetTitle() string { return "" } -func (x *Question) GetContent() string { +func (x *Question) GetText() string { if x != nil { - return x.Content + return x.Text } return "" } @@ -1239,7 +1222,7 @@ var file_github_com_hhhapz_codequest_api_v1_all_proto_rawDesc = []byte{ 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, - 0x22, 0xa8, 0x02, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, + 0x22, 0xfb, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, @@ -1247,224 +1230,218 @@ var file_github_com_hhhapz_codequest_api_v1_all_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x24, 0x0a, 0x07, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, - 0x30, 0x00, 0x50, 0x00, 0x52, 0x07, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x2b, 0x0a, - 0x0a, 0x47, 0x72, 0x61, 0x64, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x0b, 0x67, - 0x72, 0x61, 0x64, 0x65, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x20, 0x0a, 0x05, 0x41, 0x64, - 0x6d, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, - 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x45, 0x0a, 0x09, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x0a, 0x08, 0x00, 0x18, - 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x5f, 0x61, 0x74, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x8b, 0x01, 0x0a, 0x08, - 0x50, 0x61, 0x72, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x04, 0x50, 0x61, 0x72, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, - 0x50, 0x00, 0x52, 0x04, 0x70, 0x61, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x09, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, 0x00, 0x18, - 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x12, 0x2d, 0x0a, 0x0b, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x57, 0x6f, 0x72, 0x74, - 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, - 0x00, 0x50, 0x00, 0x52, 0x0c, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x5f, 0x77, 0x6f, 0x72, 0x74, - 0x68, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0xf8, 0x01, 0x0a, 0x08, 0x51, 0x75, - 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x20, 0x0a, 0x05, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x74, - 0x69, 0x74, 0x6c, 0x65, 0x12, 0x24, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, - 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x05, 0x50, 0x61, - 0x72, 0x74, 0x31, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x68, 0x68, 0x61, - 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x50, 0x61, 0x72, 0x74, 0x44, 0x61, 0x74, 0x61, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, - 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x74, 0x31, 0x12, 0x3f, 0x0a, 0x05, 0x50, - 0x61, 0x72, 0x74, 0x32, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x68, 0x68, - 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x50, 0x61, 0x72, 0x74, 0x44, 0x61, 0x74, 0x61, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, - 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x74, 0x32, 0x3a, 0x06, 0x08, 0x00, - 0x10, 0x00, 0x18, 0x00, 0x22, 0x64, 0x0a, 0x11, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x09, 0x51, 0x75, 0x65, - 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, - 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0a, 0x08, 0x00, 0x18, - 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x09, 0x71, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x39, 0x0a, 0x13, 0x51, 0x75, - 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, - 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, 0x69, 0x64, 0x3a, 0x06, 0x08, - 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x3a, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x30, 0x00, 0x50, 0x00, 0x52, 0x07, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x20, 0x0a, + 0x05, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, 0x00, + 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x12, + 0x45, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x0a, + 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x6b, + 0x0a, 0x08, 0x50, 0x61, 0x72, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x28, 0x0a, 0x09, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, + 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x64, 0x12, 0x2d, 0x0a, 0x0b, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x57, 0x6f, + 0x72, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, + 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x0c, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x5f, 0x77, 0x6f, + 0x72, 0x74, 0x68, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0xf2, 0x01, 0x0a, 0x08, + 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x05, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, + 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x1e, 0x0a, 0x04, 0x54, 0x65, 0x78, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, + 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x3f, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x74, 0x31, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x72, 0x74, + 0x44, 0x61, 0x74, 0x61, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, + 0x52, 0x05, 0x70, 0x61, 0x72, 0x74, 0x31, 0x12, 0x3f, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x74, 0x32, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x72, + 0x74, 0x44, 0x61, 0x74, 0x61, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, + 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x74, 0x32, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, + 0x22, 0x64, 0x0a, 0x11, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x09, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, + 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, + 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, + 0x00, 0x50, 0x00, 0x52, 0x09, 0x71, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x06, + 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x39, 0x0a, 0x13, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, + 0x6f, 0x6e, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, 0x69, 0x64, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, - 0x00, 0x22, 0x55, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, - 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, - 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, - 0x0a, 0x05, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, - 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, - 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x98, 0x01, 0x0a, 0x0d, 0x53, 0x75, 0x62, - 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, - 0x50, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x06, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, - 0x50, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x04, 0x50, - 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, - 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x04, 0x70, 0x61, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x04, 0x43, - 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, - 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x06, 0x08, 0x00, 0x10, - 0x00, 0x18, 0x00, 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, + 0x00, 0x22, 0x3a, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, + 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, + 0x00, 0x52, 0x02, 0x69, 0x64, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x55, 0x0a, + 0x0d, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, + 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, + 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x05, 0x49, 0x6e, + 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, + 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x06, 0x08, 0x00, + 0x10, 0x00, 0x18, 0x00, 0x22, 0x98, 0x01, 0x0a, 0x0d, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x50, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, - 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x04, 0x70, 0x61, - 0x72, 0x74, 0x12, 0x24, 0x0a, 0x07, 0x43, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, + 0x69, 0x64, 0x12, 0x23, 0x0a, 0x06, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x07, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x04, 0x50, 0x61, 0x72, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, + 0x00, 0x52, 0x04, 0x70, 0x61, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, + 0x00, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, + 0xc4, 0x01, 0x0a, 0x0e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, + 0x0a, 0x04, 0x50, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, + 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x04, 0x70, 0x61, 0x72, 0x74, 0x12, 0x24, + 0x0a, 0x07, 0x43, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x42, + 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x72, + 0x72, 0x65, 0x63, 0x74, 0x12, 0x24, 0x0a, 0x07, 0x52, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, + 0x00, 0x52, 0x07, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x12, 0x22, 0x0a, 0x06, 0x50, 0x6f, + 0x69, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, + 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x06, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x3a, 0x06, + 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x85, 0x01, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x1e, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, + 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x0a, 0x47, 0x72, 0x61, 0x64, 0x65, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, + 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x64, 0x65, 0x5f, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x20, 0x0a, 0x05, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, - 0x07, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x12, 0x24, 0x0a, 0x07, 0x52, 0x61, 0x6e, 0x6b, - 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, - 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x07, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x12, 0x22, - 0x0a, 0x06, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, - 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x06, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x73, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x85, 0x01, 0x0a, 0x0c, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x1e, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, - 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x0a, 0x47, - 0x72, 0x61, 0x64, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, - 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x0b, 0x67, 0x72, 0x61, - 0x64, 0x65, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x20, 0x0a, 0x05, 0x41, 0x64, 0x6d, 0x69, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, - 0x00, 0x50, 0x00, 0x52, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, - 0x18, 0x00, 0x22, 0x15, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x19, 0x0a, 0x0f, 0x41, 0x6c, 0x6c, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x3a, 0x06, 0x08, 0x00, - 0x10, 0x00, 0x18, 0x00, 0x22, 0x82, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, + 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x15, + 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x3a, 0x06, 0x08, + 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x19, 0x0a, 0x0f, 0x41, 0x6c, 0x6c, 0x55, 0x73, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, + 0x22, 0x82, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x05, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, + 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x43, 0x0a, 0x04, 0x42, 0x6f, 0x64, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, + 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x3a, 0x06, 0x08, + 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x3e, 0x0a, 0x12, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x45, + 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x05, 0x45, + 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, + 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x3a, 0x06, 0x08, + 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x3d, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x05, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, - 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x43, 0x0a, 0x04, - 0x42, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x68, 0x68, 0x68, - 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x42, 0x0a, 0x08, - 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, - 0x73, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x3e, 0x0a, 0x12, 0x55, 0x73, 0x65, - 0x72, 0x42, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x20, 0x0a, 0x05, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, - 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, - 0x6c, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x3d, 0x0a, 0x11, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, - 0x0a, 0x05, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x08, - 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, - 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x22, 0x57, 0x0a, 0x10, 0x41, 0x6c, 0x6c, 0x55, - 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x05, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x68, - 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, - 0x50, 0x00, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, - 0x00, 0x32, 0xe9, 0x02, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x7b, 0x0a, 0x09, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, - 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x41, 0x75, 0x74, - 0x68, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x88, - 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x76, 0x31, - 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x28, 0x00, 0x30, 0x00, 0x12, 0x68, - 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x68, 0x68, 0x68, - 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x1c, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x28, 0x00, 0x30, 0x00, 0x12, 0x6e, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x27, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1c, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x2a, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, - 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x28, 0x00, 0x1a, 0x03, 0x88, 0x02, 0x00, 0x32, 0x84, 0x04, - 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6a, - 0x0a, 0x09, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x26, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x88, 0x02, 0x00, - 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x00, 0x12, 0x7d, 0x0a, 0x0c, 0x51, 0x75, - 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x49, 0x44, 0x12, 0x28, 0x2e, 0x68, 0x68, 0x68, + 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x3a, 0x06, 0x08, 0x00, + 0x10, 0x00, 0x18, 0x00, 0x22, 0x57, 0x0a, 0x10, 0x41, 0x6c, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x05, 0x55, 0x73, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x42, 0x0a, 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x30, 0x00, 0x50, 0x00, 0x52, 0x05, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x3a, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x32, 0xe9, 0x02, + 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a, + 0x09, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x20, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x14, 0x12, 0x12, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x28, 0x00, 0x30, 0x00, 0x12, 0x8a, 0x01, 0x0a, 0x0d, 0x51, 0x75, - 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x29, 0x2e, 0x68, 0x68, - 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, + 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x88, 0x02, 0x00, 0x90, 0x02, + 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, + 0x68, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x28, 0x00, 0x30, 0x00, 0x12, 0x68, 0x0a, 0x05, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x22, 0x1c, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, + 0x12, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x28, 0x00, 0x30, 0x00, 0x12, 0x6e, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x27, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1c, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x10, 0x2a, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x28, 0x00, 0x1a, 0x03, 0x88, 0x02, 0x00, 0x32, 0x84, 0x04, 0x0a, 0x0c, 0x51, 0x75, + 0x65, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6a, 0x0a, 0x09, 0x51, 0x75, + 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x26, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x00, 0x12, 0x7d, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, + 0x6f, 0x6e, 0x42, 0x79, 0x49, 0x44, 0x12, 0x28, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, - 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x26, 0x88, 0x02, 0x00, 0x90, - 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x2f, 0x69, 0x6e, 0x70, - 0x75, 0x74, 0x28, 0x00, 0x30, 0x00, 0x12, 0x77, 0x0a, 0x06, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, - 0x12, 0x22, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x88, 0x02, 0x00, 0x90, 0x02, - 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x22, 0x12, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x28, 0x00, 0x30, 0x00, 0x1a, - 0x03, 0x88, 0x02, 0x00, 0x32, 0xec, 0x05, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x68, + 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x20, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, + 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x49, 0x44, + 0x7d, 0x28, 0x00, 0x30, 0x00, 0x12, 0x8a, 0x01, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x29, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, + 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x26, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, 0x73, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x2f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x28, 0x00, + 0x30, 0x00, 0x12, 0x77, 0x0a, 0x06, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x12, 0x22, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, - 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x22, 0x1a, 0x88, 0x02, 0x00, 0x90, 0x02, - 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, - 0x72, 0x73, 0x2f, 0x6d, 0x65, 0x28, 0x00, 0x30, 0x00, 0x12, 0x7c, 0x0a, 0x0b, 0x55, 0x73, 0x65, - 0x72, 0x42, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x27, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, + 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x23, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x14, 0x22, 0x12, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x28, 0x00, 0x30, 0x00, 0x1a, 0x03, 0x88, 0x02, 0x00, + 0x32, 0xec, 0x05, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x63, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x73, 0x65, 0x72, 0x42, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x22, 0x25, 0x88, 0x02, - 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x76, 0x31, 0x2f, - 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x45, 0x6d, 0x61, - 0x69, 0x6c, 0x7d, 0x28, 0x00, 0x30, 0x00, 0x12, 0x7a, 0x0a, 0x08, 0x41, 0x6c, 0x6c, 0x55, 0x73, - 0x65, 0x72, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x6c, 0x55, 0x73, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x68, 0x68, 0x61, - 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x6c, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x1d, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, - 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x28, - 0x00, 0x30, 0x00, 0x12, 0x75, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x12, 0x26, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, - 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x22, 0x20, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x14, 0x3a, 0x04, 0x42, 0x6f, 0x64, 0x79, 0x1a, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, - 0x65, 0x72, 0x73, 0x2f, 0x6d, 0x65, 0x28, 0x00, 0x30, 0x00, 0x12, 0x85, 0x01, 0x0a, 0x0f, 0x41, - 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x26, - 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, - 0x72, 0x22, 0x2b, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x3a, - 0x04, 0x42, 0x6f, 0x64, 0x79, 0x1a, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, - 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x7d, 0x28, 0x00, - 0x30, 0x00, 0x12, 0x7a, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, - 0x12, 0x26, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, - 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x73, 0x65, 0x72, 0x22, 0x25, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x19, 0x2a, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x75, 0x73, 0x65, - 0x72, 0x73, 0x2f, 0x7b, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x7d, 0x28, 0x00, 0x30, 0x00, 0x1a, 0x03, - 0x88, 0x02, 0x00, 0x42, 0x47, 0x48, 0x01, 0x50, 0x00, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, - 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x80, 0x01, 0x00, 0x88, 0x01, 0x00, 0x90, 0x01, 0x00, - 0xb8, 0x01, 0x00, 0xd8, 0x01, 0x00, 0xf8, 0x01, 0x01, 0xd0, 0x02, 0x00, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, + 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x22, 0x1a, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x6d, + 0x65, 0x28, 0x00, 0x30, 0x00, 0x12, 0x7c, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x45, + 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x27, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x42, + 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, + 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x22, 0x25, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, + 0x6e, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x7d, 0x28, + 0x00, 0x30, 0x00, 0x12, 0x7a, 0x0a, 0x08, 0x41, 0x6c, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, + 0x24, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x6c, 0x55, + 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x88, 0x02, + 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x76, 0x31, 0x2f, + 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x28, 0x00, 0x30, 0x00, 0x12, + 0x75, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x26, 0x2e, + 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, + 0x22, 0x20, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x04, + 0x42, 0x6f, 0x64, 0x79, 0x1a, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, + 0x6d, 0x65, 0x28, 0x00, 0x30, 0x00, 0x12, 0x85, 0x01, 0x0a, 0x0f, 0x41, 0x64, 0x6d, 0x69, 0x6e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x26, 0x2e, 0x68, 0x68, 0x68, + 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x22, 0x2b, 0x88, + 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x3a, 0x04, 0x42, 0x6f, 0x64, + 0x79, 0x1a, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x2f, 0x7b, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x7d, 0x28, 0x00, 0x30, 0x00, 0x12, 0x7a, + 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x26, 0x2e, 0x68, + 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x22, + 0x25, 0x88, 0x02, 0x00, 0x90, 0x02, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, + 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x7d, 0x28, 0x00, 0x30, 0x00, 0x1a, 0x03, 0x88, 0x02, 0x00, 0x42, + 0x47, 0x48, 0x01, 0x50, 0x00, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x68, 0x68, 0x68, 0x61, 0x70, 0x7a, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x64, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x80, 0x01, 0x00, 0x88, 0x01, 0x00, 0x90, 0x01, 0x00, 0xb8, 0x01, 0x00, 0xd8, + 0x01, 0x00, 0xf8, 0x01, 0x01, 0xd0, 0x02, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/api/v1/codequest.gunk b/api/v1/codequest.gunk index bbf9417..cd0680d 100644 --- a/api/v1/codequest.gunk +++ b/api/v1/codequest.gunk @@ -18,8 +18,6 @@ type User struct { Email string `pb:"3" json:"email"` // Picture is the URL of the user's profile picture. Picture string `pb:"4" json:"picture"` - // GradeLevel of the user. - GradeLevel int `pb:"5" json:"grade_level"` // Admin is true if the user is an administrator. Admin bool `pb:"6" json:"admin"` // CreatedAt is the time the user was created. diff --git a/api/v1/gen/json/codequest/all.swagger.json b/api/v1/gen/json/codequest/all.swagger.json index 706f30b..798e762 100644 --- a/api/v1/gen/json/codequest/all.swagger.json +++ b/api/v1/gen/json/codequest/all.swagger.json @@ -474,10 +474,6 @@ "v1PartData": { "type": "object", "properties": { - "part": { - "type": "integer", - "format": "int32" - }, "completed": { "type": "boolean" }, @@ -496,7 +492,7 @@ "title": { "type": "string" }, - "content": { + "text": { "type": "string" }, "part1": { @@ -598,11 +594,6 @@ "type": "string", "description": "Picture is the URL of the user's profile picture." }, - "grade_level": { - "type": "integer", - "format": "int32", - "description": "GradeLevel of the user." - }, "admin": { "type": "boolean", "description": "Admin is true if the user is an administrator." diff --git a/api/v1/gen/ts-gateway/codequest/all.pb.ts b/api/v1/gen/ts-gateway/codequest/all.pb.ts index 67d4e4b..7cefc7b 100644 --- a/api/v1/gen/ts-gateway/codequest/all.pb.ts +++ b/api/v1/gen/ts-gateway/codequest/all.pb.ts @@ -34,13 +34,11 @@ export type User = { name?: string email?: string picture?: string - gradeLevel?: number admin?: boolean createdAt?: GoogleProtobufTimestamp.Timestamp } export type PartData = { - part?: number completed?: boolean pointsWorth?: number } @@ -48,7 +46,7 @@ export type PartData = { export type Question = { id?: string title?: string - content?: string + text?: string part1?: PartData part2?: PartData } diff --git a/api/v1/gen/ts/codequest/all_pb.d.ts b/api/v1/gen/ts/codequest/all_pb.d.ts index 0bcc797..2c673a4 100644 --- a/api/v1/gen/ts/codequest/all_pb.d.ts +++ b/api/v1/gen/ts/codequest/all_pb.d.ts @@ -131,9 +131,6 @@ export class User extends jspb.Message { getPicture(): string; setPicture(value: string): void; - getGradelevel(): number; - setGradelevel(value: number): void; - getAdmin(): boolean; setAdmin(value: boolean): void; @@ -158,16 +155,12 @@ export namespace User { name: string, email: string, picture: string, - gradelevel: number, admin: boolean, createdat?: google_protobuf_timestamp_pb.Timestamp.AsObject, } } export class PartData extends jspb.Message { - getPart(): number; - setPart(value: number): void; - getCompleted(): boolean; setCompleted(value: boolean): void; @@ -186,7 +179,6 @@ export class PartData extends jspb.Message { export namespace PartData { export type AsObject = { - part: number, completed: boolean, pointsworth: number, } @@ -199,8 +191,8 @@ export class Question extends jspb.Message { getTitle(): string; setTitle(value: string): void; - getContent(): string; - setContent(value: string): void; + getText(): string; + setText(value: string): void; hasPart1(): boolean; clearPart1(): void; @@ -226,7 +218,7 @@ export namespace Question { export type AsObject = { id: string, title: string, - content: string, + text: string, part1?: PartData.AsObject, part2?: PartData.AsObject, } diff --git a/api/v1/quest.gunk b/api/v1/quest.gunk index 3582912..384223c 100644 --- a/api/v1/quest.gunk +++ b/api/v1/quest.gunk @@ -34,17 +34,16 @@ type QuestService interface { } type PartData struct { - Part int `pb:"1" json:"part"` Completed bool `pb:"2" json:"completed"` PointsWorth int `pb:"3" json:"points_worth"` } type Question struct { - ID string `pb:"1" json:"id"` - Title string `pb:"2" json:"title"` - Content string `pb:"3" json:"content"` - Part1 PartData `pb:"4" json:"part1"` - Part2 PartData `pb:"5" json:"part2"` + ID string `pb:"1" json:"id"` + Title string `pb:"2" json:"title"` + Text string `pb:"3" json:"text"` + Part1 PartData `pb:"4" json:"part1"` + Part2 PartData `pb:"5" json:"part2"` } type QuestionsResponse struct { diff --git a/cmd/srv/main.go b/cmd/srv/main.go index e7d2528..46c78d8 100644 --- a/cmd/srv/main.go +++ b/cmd/srv/main.go @@ -4,13 +4,19 @@ import ( "flag" "fmt" "io" + "log" "net" "os" "github.com/hhhapz/codequest/api" "github.com/hhhapz/codequest/db" + "github.com/hhhapz/codequest/models" "github.com/peterbourgon/ff/v3" "google.golang.org/grpc/grpclog" + + // questions + _ "github.com/hhhapz/codequest/question/q01" + _ "github.com/hhhapz/codequest/question/q02" ) func run() error { @@ -30,13 +36,14 @@ func run() error { if err != nil { return fmt.Errorf("could not open db: %v", err) } + models.SetLogger(log.Printf) oaStore, err := db.NewOAuthState(*secretFile) if err != nil { return fmt.Errorf("could not create oauth config: %w", err) } - server, err := api.NewServer(oaStore, database) + server, err := api.NewServer(oaStore, database, database) log := grpclog.NewLoggerV2(os.Stderr, io.Discard, os.Stderr) grpclog.SetLoggerV2(log) diff --git a/db/quest.go b/db/quest.go new file mode 100644 index 0000000..a27573c --- /dev/null +++ b/db/quest.go @@ -0,0 +1,119 @@ +package db + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/hhhapz/codequest/models" + "github.com/hhhapz/codequest/question" +) + +func (db *DB) Questions(ctx context.Context, u *models.User) ([]*question.Data, error) { + ui, err := models.UserInfoByUserID(ctx, db.DB, u.ID) + if errors.Is(err, sql.ErrNoRows) { + return nil, models.NewUserError("you must fill in your user details first") + } + if err != nil { + return nil, err + } + + var questions []*question.Question + for _, q := range question.Questions { + if ui.SkillLevel != int(q.Level) { + continue + } + questions = append(questions, q) + } + + parts, err := models.AllQuestionPartsData(ctx, db.DB, u.ID) + if err != nil { + return nil, err + } + + var data []*question.Data + for _, q := range questions { + var p1, p2 question.PartData + for _, p := range parts { + if p.QuestionID == q.ID { + p1, p2 = question.PartData{ + Completed: p.P1Awarded.Valid, + PointsWorth: int(p.P1Awarded.Int64), + }, question.PartData{ + Completed: p.P2Awarded.Valid, + PointsWorth: int(p.P2Awarded.Int64), + } + } + break + } + + data = append(data, &question.Data{ + QuestionID: q.ID, + UserID: u.ID, + Name: q.Name, + Part1: p1, + Part2: p2, + }) + } + + return data, nil +} + +func (db *DB) Question(ctx context.Context, u *models.User, id string) (*question.Data, error) { + ui, err := models.UserInfoByUserID(ctx, db.DB, u.ID) + if errors.Is(err, sql.ErrNoRows) { + return nil, models.NewUserError("you must fill in your user details first") + } + if err != nil { + return nil, err + } + + var q *question.Question + for _, qq := range question.Questions { + if qq.ID != id { + continue + } + if ui.SkillLevel != int(qq.Level) { + return nil, models.NewUserError("this question is not available for your skill level") + } + q = qq + break + } + if q == nil { + return nil, models.NewUserError("question with the id %q not found", id) + } + + partData, err := models.QuestionPartsData(ctx, db.DB, q.ID, u.ID) + if err != nil { + return nil, fmt.Errorf("could not retrieve question data: %w", err) + } + + return &question.Data{ + QuestionID: q.ID, + UserID: u.ID, + Name: q.Name, + Text: q.Text, + Level: q.Level, + Part1: question.PartData{ + Completed: partData.P1Awarded.Valid, + PointsWorth: int(partData.P1Awarded.Int64), + }, + Part2: question.PartData{ + Completed: partData.P2Awarded.Valid, + PointsWorth: int(partData.P2Awarded.Int64), + }, + }, nil +} + +func (db *DB) Submissions(ctx context.Context, u *models.User, questionID string, part question.Part) (bool, int, error) { + s, err := models.QuestionSubmissions(ctx, db.DB, u.ID, questionID, int(part)) + if err != nil { + return false, 0, err + } + return s.Correct, s.ByUser, nil +} + +func (db *DB) AddSubmission(ctx context.Context, submission *models.QuestionAttempt) error { + return submission.Insert(ctx, db.DB) +} diff --git a/gen.sh b/gen.sh index 7afa37a..71e68f0 100755 --- a/gen.sh +++ b/gen.sh @@ -57,3 +57,70 @@ DELETE FROM tokens WHERE user_id = %%user_id string%% ENDSQL + +FIELDS='QuestionID string,P1Awarded sql.NullInt64,P2Awarded sql.NullInt64' +xo query $DB -M -B -2 -Z "$FIELDS" -T AllPartsData -F AllQuestionPartsData -o $DEST << ENDSQL +WITH attempts AS ( + SELECT * + FROM question_attempt + WHERE correct = true +) +SELECT DISTINCT + a.question_id as question_id, + MAX(p1.points_awarded) AS p1_awarded, + MAX(p2.points_awarded) AS p2_awarded +FROM attempts a + +LEFT JOIN attempts AS p1 +ON p1.id = a.id +AND p1.question_part = 1 + +LEFT JOIN attempts AS p2 +ON p2.id = a.id +AND p2.question_part = 2 + +WHERE a.user_id = %%user_id string%% + +GROUP BY a.user_id, a.question_id +ENDSQL + +xo query $DB -M -B -2 -1 -T PartsData -F QuestionPartsData -o $DEST << ENDSQL +WITH attempts AS ( + SELECT * + FROM question_attempt + WHERE question_id = %%question_id string%% + AND correct = true +) +SELECT + p1.points_awarded AS p1_awarded, + p2.points_awarded AS p2_awarded +FROM users u + +LEFT JOIN attempts AS p1 +ON p1.user_id = u.id +AND p1.question_part = 1 + +LEFT JOIN attempts AS p2 +ON p2.user_id = u.id +AND p2.question_part = 2 + +WHERE + u.id = %%user_id string%% +ENDSQL + +FIELDS='ByUser int,Correct bool' +xo query $DB -M -B -2 -1 -Z "$FIELDS" -T Submissions -F QuestionSubmissions -o $DEST << ENDSQL +WITH attempts AS ( + SELECT * FROM question_attempt + WHERE user_id = %%user_id string%% + AND question_id = %%question_id string%% + AND question_part = %%question_part int%% +) +SELECT + c.count AS by_user, + COALESCE(a.correct, FALSE) AS correct +FROM ( + SELECT COUNT(*) as count from attempts +) c +LEFT JOIN attempts a ON a.correct; +ENDSQL diff --git a/go.mod b/go.mod index c3b7004..67aed05 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module github.com/hhhapz/codequest go 1.17 require ( + github.com/google/uuid v1.1.2 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0 + github.com/k0kubun/pp/v3 v3.0.7 + github.com/kenshaw/redoc v0.1.3 github.com/mattn/go-sqlite3 v1.14.8 github.com/peterbourgon/ff/v3 v3.1.0 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 @@ -24,10 +27,10 @@ require ( github.com/gunk/opt v0.2.0 // indirect github.com/kenshaw/diskcache v0.5.1 // indirect github.com/kenshaw/httplog v0.4.0 // indirect - github.com/kenshaw/redoc v0.1.3 // indirect github.com/kenshaw/webfonts v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.7 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect github.com/spf13/afero v1.6.0 // indirect - github.com/stretchr/testify v1.7.0 // indirect github.com/tdewolff/minify/v2 v2.9.22 // indirect github.com/tdewolff/parse/v2 v2.5.22 // indirect github.com/vanng822/css v1.0.1 // indirect diff --git a/go.sum b/go.sum index 1eab587..69e7c6a 100644 --- a/go.sum +++ b/go.sum @@ -23,7 +23,6 @@ cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSU cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1 h1:DwuSvDZ1pTYGbXo8yOJevCTr3BoBlE+OVkHAKiYQUXc= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= @@ -81,6 +80,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -162,10 +162,10 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0 h1:6DWmvNpomjL1+3liNSZbVns3zsYzzCjm6pRBO1tLeso= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= @@ -185,8 +185,13 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp/v3 v3.0.7 h1:Qj4zVxA0ceXq0mfNbHwFPye58UyabBWi3emM2SwBT5Y= +github.com/k0kubun/pp/v3 v3.0.7/go.mod h1:2ol0zQBSPTermAo8igHVJ4d5vTiNmBkCrUdu7wZp4aI= +github.com/kenshaw/diskcache v0.5.0/go.mod h1:niKrpaT3MnShxQ4OojUoT8VNr9e5r+SQf8nrzVWjIrQ= github.com/kenshaw/diskcache v0.5.1 h1:mdgyJGiGOn+1ZW6WOa7u+HwsJoExiRzEBymjjTBbOJc= github.com/kenshaw/diskcache v0.5.1/go.mod h1:2g5XF+9Dgd1iBFi0anYYw3beLaLnTgJSSzsKIuk+Z5E= +github.com/kenshaw/httplog v0.3.2/go.mod h1:1mIAV0a4k32ePm32Ve3oVzzCfPje66oWCLfUOA4l8Pw= github.com/kenshaw/httplog v0.4.0 h1:6gevB91JwSsEKB+Q10zxv392t4bLcab/HxfVYBJ0ohs= github.com/kenshaw/httplog v0.4.0/go.mod h1:O0bRNzPagLH+kWMB9f+rwFwmjT4MfKcuTy4D6q4/2rU= github.com/kenshaw/redoc v0.1.3 h1:GP5yi6+Z8Eyoa3ZwYzOFUnxkBZk/wwTAhZq9VdTj11M= @@ -202,6 +207,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= +github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -228,11 +237,14 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tdewolff/minify/v2 v2.9.21/go.mod h1:PoDBts2L7sCwUT28vTAlozGeD6qxjrrihtin4bR/RMM= github.com/tdewolff/minify/v2 v2.9.22 h1:PlmaAakaJHdMMdTTwjjsuSwIxKqWPTlvjTj6a/g/ILU= github.com/tdewolff/minify/v2 v2.9.22/go.mod h1:dNlaFdXaIxgSXh3UFASqjTY0/xjpDkkCsYHA1NCGnmQ= +github.com/tdewolff/parse/v2 v2.5.19/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/parse/v2 v2.5.21/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/parse/v2 v2.5.22 h1:KXMHTyx4VTL6Zu9a94SULQalDMvtP5FQq10mnSfaoGs= github.com/tdewolff/parse/v2 v2.5.22/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8= github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= @@ -331,8 +343,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211014222326-fd004c51d1d6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211116231205-47ca1ff31462 h1:2vmJlzGKvQ7e/X9XT0XydeWDxmqx8DnegiIMRT+5ssI= golang.org/x/net v0.0.0-20211116231205-47ca1ff31462/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -351,8 +362,6 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 h1:v79phzBz03tsVCUTbvTBmmC3CUXF5mKYt7DA4ZVldpM= -golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -377,9 +386,11 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -412,6 +423,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c h1:DHcbWVXeY+0Y8HHKR+rbLwnoh2F4tNCY7rTiHJ30RmA= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -515,8 +528,8 @@ google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00 google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0 h1:4t9zuDlHLcIx0ZEhmXEeFVCRsiOgpgn2QOH9N0MNjPI= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= google.golang.org/api v0.60.0 h1:eq/zs5WPH4J9undYM9IP1O7dSr7Yh8Y0GtSCpzGzIUk= google.golang.org/api v0.60.0/go.mod h1:d7rl65NZAkEQ90JFzqBjcRq1TVeG5ZoGV3sSpEnnVb4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -583,7 +596,9 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211116182654-e63d96a377c4 h1:nPiLDJ9/wsay2NDshdJ1B24frx+butTxmaVaCxDBChY= google.golang.org/genproto v0.0.0-20211116182654-e63d96a377c4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -611,8 +626,8 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= diff --git a/models/allpartsdata.xo.go b/models/allpartsdata.xo.go new file mode 100644 index 0000000..46ba9c2 --- /dev/null +++ b/models/allpartsdata.xo.go @@ -0,0 +1,63 @@ +package models + +// Code generated by xo. DO NOT EDIT. + +import ( + "context" + "database/sql" +) + +// AllPartsData represents a row from 'all_parts_data'. +type AllPartsData struct { + QuestionID string `json:"question_id"` // question_id + P1Awarded sql.NullInt64 `json:"p1awarded"` // p1awarded + P2Awarded sql.NullInt64 `json:"p2awarded"` // p2awarded +} + +// AllQuestionPartsData runs a custom query, returning results as AllPartsData. +func AllQuestionPartsData(ctx context.Context, db DB, user_id string) ([]*AllPartsData, error) { + // query + const sqlstr = `WITH attempts AS ( ` + + `SELECT * ` + + `FROM question_attempt ` + + `WHERE correct = true ` + + `) ` + + `SELECT DISTINCT ` + + `a.question_id as question_id, ` + + `MAX(p1.points_awarded) AS p1_awarded, ` + + `MAX(p2.points_awarded) AS p2_awarded ` + + `FROM attempts a ` + + ` ` + + `LEFT JOIN attempts AS p1 ` + + `ON p1.id = a.id ` + + `AND p1.question_part = 1 ` + + ` ` + + `LEFT JOIN attempts AS p2 ` + + `ON p2.id = a.id ` + + `AND p2.question_part = 2 ` + + ` ` + + `WHERE a.user_id = $1 ` + + ` ` + + `GROUP BY a.user_id, a.question_id` + // run + logf(sqlstr, user_id) + rows, err := db.QueryContext(ctx, sqlstr, user_id) + if err != nil { + return nil, logerror(err) + } + defer rows.Close() + // load results + var res []*AllPartsData + for rows.Next() { + var apd AllPartsData + // scan + if err := rows.Scan(&apd.QuestionID, &apd.P1Awarded, &apd.P2Awarded); err != nil { + return nil, logerror(err) + } + res = append(res, &apd) + } + if err := rows.Err(); err != nil { + return nil, logerror(err) + } + return res, nil +} diff --git a/models/partsdata.xo.go b/models/partsdata.xo.go new file mode 100644 index 0000000..a68e915 --- /dev/null +++ b/models/partsdata.xo.go @@ -0,0 +1,47 @@ +package models + +// Code generated by xo. DO NOT EDIT. + +import ( + "context" + "database/sql" +) + +// PartsData represents a row from 'parts_data'. +type PartsData struct { + P1Awarded sql.NullInt64 `json:"p1_awarded"` // p1_awarded + P2Awarded sql.NullInt64 `json:"p2_awarded"` // p2_awarded +} + +// QuestionPartsData runs a custom query, returning results as PartsData. +func QuestionPartsData(ctx context.Context, db DB, question_id, user_id string) (*PartsData, error) { + // query + const sqlstr = `WITH attempts AS ( ` + + `SELECT * ` + + `FROM question_attempt ` + + `WHERE question_id = $1 ` + + `AND correct = true ` + + `) ` + + `SELECT ` + + `p1.points_awarded AS p1_awarded, ` + + `p2.points_awarded AS p2_awarded ` + + `FROM users u ` + + ` ` + + `LEFT JOIN attempts AS p1 ` + + `ON p1.user_id = u.id ` + + `AND p1.question_part = 1 ` + + ` ` + + `LEFT JOIN attempts AS p2 ` + + `ON p2.user_id = u.id ` + + `AND p2.question_part = 2 ` + + ` ` + + `WHERE ` + + `u.id = $2` + // run + logf(sqlstr, question_id, user_id) + var pd PartsData + if err := db.QueryRowContext(ctx, sqlstr, question_id, user_id).Scan(&pd.P1Awarded, &pd.P2Awarded); err != nil { + return nil, logerror(err) + } + return &pd, nil +} diff --git a/models/questionattempt.xo.go b/models/questionattempt.xo.go new file mode 100644 index 0000000..a36bec4 --- /dev/null +++ b/models/questionattempt.xo.go @@ -0,0 +1,165 @@ +package models + +// Code generated by xo. DO NOT EDIT. + +import ( + "context" +) + +// QuestionAttempt represents a row from 'question_attempt'. +type QuestionAttempt struct { + ID int `json:"id"` // id + UserID string `json:"user_id"` // user_id + QuestionID string `json:"question_id"` // question_id + QuestionPart int `json:"question_part"` // question_part + Correct bool `json:"correct"` // correct + PointsAwarded int `json:"points_awarded"` // points_awarded + Input string `json:"input"` // input + Code string `json:"code"` // code + SubmittedAt Time `json:"submitted_at"` // submitted_at + // xo fields + _exists, _deleted bool +} + +// Exists returns true when the QuestionAttempt exists in the database. +func (qa *QuestionAttempt) Exists() bool { + return qa._exists +} + +// Deleted returns true when the QuestionAttempt has been marked for deletion from +// the database. +func (qa *QuestionAttempt) Deleted() bool { + return qa._deleted +} + +// Insert inserts the QuestionAttempt to the database. +func (qa *QuestionAttempt) Insert(ctx context.Context, db DB) error { + switch { + case qa._exists: // already exists + return logerror(&ErrInsertFailed{ErrAlreadyExists}) + case qa._deleted: // deleted + return logerror(&ErrInsertFailed{ErrMarkedForDeletion}) + } + // insert (primary key generated and returned by database) + const sqlstr = `INSERT INTO question_attempt (` + + `user_id, question_id, question_part, correct, points_awarded, input, code, submitted_at` + + `) VALUES (` + + `$1, $2, $3, $4, $5, $6, $7, $8` + + `)` + // run + logf(sqlstr, qa.UserID, qa.QuestionID, qa.QuestionPart, qa.Correct, qa.PointsAwarded, qa.Input, qa.Code, qa.SubmittedAt) + res, err := db.ExecContext(ctx, sqlstr, qa.UserID, qa.QuestionID, qa.QuestionPart, qa.Correct, qa.PointsAwarded, qa.Input, qa.Code, qa.SubmittedAt) + if err != nil { + return logerror(err) + } + // retrieve id + id, err := res.LastInsertId() + if err != nil { + return logerror(err) + } // set primary key + qa.ID = int(id) + // set exists + qa._exists = true + return nil +} + +// Update updates a QuestionAttempt in the database. +func (qa *QuestionAttempt) Update(ctx context.Context, db DB) error { + switch { + case !qa._exists: // doesn't exist + return logerror(&ErrUpdateFailed{ErrDoesNotExist}) + case qa._deleted: // deleted + return logerror(&ErrUpdateFailed{ErrMarkedForDeletion}) + } + // update with primary key + const sqlstr = `UPDATE question_attempt SET ` + + `user_id = $1, question_id = $2, question_part = $3, correct = $4, points_awarded = $5, input = $6, code = $7, submitted_at = $8 ` + + `WHERE id = $9` + // run + logf(sqlstr, qa.UserID, qa.QuestionID, qa.QuestionPart, qa.Correct, qa.PointsAwarded, qa.Input, qa.Code, qa.SubmittedAt, qa.ID) + if _, err := db.ExecContext(ctx, sqlstr, qa.UserID, qa.QuestionID, qa.QuestionPart, qa.Correct, qa.PointsAwarded, qa.Input, qa.Code, qa.SubmittedAt, qa.ID); err != nil { + return logerror(err) + } + return nil +} + +// Save saves the QuestionAttempt to the database. +func (qa *QuestionAttempt) Save(ctx context.Context, db DB) error { + if qa.Exists() { + return qa.Update(ctx, db) + } + return qa.Insert(ctx, db) +} + +// Upsert performs an upsert for QuestionAttempt. +func (qa *QuestionAttempt) Upsert(ctx context.Context, db DB) error { + switch { + case qa._deleted: // deleted + return logerror(&ErrUpsertFailed{ErrMarkedForDeletion}) + } + // upsert + const sqlstr = `INSERT INTO question_attempt (` + + `id, user_id, question_id, question_part, correct, points_awarded, input, code, submitted_at` + + `) VALUES (` + + `$1, $2, $3, $4, $5, $6, $7, $8, $9` + + `)` + + ` ON CONFLICT (id) DO ` + + `UPDATE SET ` + + `user_id = EXCLUDED.user_id, question_id = EXCLUDED.question_id, question_part = EXCLUDED.question_part, correct = EXCLUDED.correct, points_awarded = EXCLUDED.points_awarded, input = EXCLUDED.input, code = EXCLUDED.code, submitted_at = EXCLUDED.submitted_at ` + // run + logf(sqlstr, qa.ID, qa.UserID, qa.QuestionID, qa.QuestionPart, qa.Correct, qa.PointsAwarded, qa.Input, qa.Code, qa.SubmittedAt) + if _, err := db.ExecContext(ctx, sqlstr, qa.ID, qa.UserID, qa.QuestionID, qa.QuestionPart, qa.Correct, qa.PointsAwarded, qa.Input, qa.Code, qa.SubmittedAt); err != nil { + return logerror(err) + } + // set exists + qa._exists = true + return nil +} + +// Delete deletes the QuestionAttempt from the database. +func (qa *QuestionAttempt) Delete(ctx context.Context, db DB) error { + switch { + case !qa._exists: // doesn't exist + return nil + case qa._deleted: // deleted + return nil + } + // delete with single primary key + const sqlstr = `DELETE FROM question_attempt ` + + `WHERE id = $1` + // run + logf(sqlstr, qa.ID) + if _, err := db.ExecContext(ctx, sqlstr, qa.ID); err != nil { + return logerror(err) + } + // set deleted + qa._deleted = true + return nil +} + +// QuestionAttemptByID retrieves a row from 'question_attempt' as a QuestionAttempt. +// +// Generated from index 'question_attempt_id_pkey'. +func QuestionAttemptByID(ctx context.Context, db DB, id int) (*QuestionAttempt, error) { + // query + const sqlstr = `SELECT ` + + `id, user_id, question_id, question_part, correct, points_awarded, input, code, submitted_at ` + + `FROM question_attempt ` + + `WHERE id = $1` + // run + logf(sqlstr, id) + qa := QuestionAttempt{ + _exists: true, + } + if err := db.QueryRowContext(ctx, sqlstr, id).Scan(&qa.ID, &qa.UserID, &qa.QuestionID, &qa.QuestionPart, &qa.Correct, &qa.PointsAwarded, &qa.Input, &qa.Code, &qa.SubmittedAt); err != nil { + return nil, logerror(err) + } + return &qa, nil +} + +// User returns the User associated with the QuestionAttempt's (UserID). +// +// Generated from foreign key 'question_attempt_user_id_fkey'. +func (qa *QuestionAttempt) User(ctx context.Context, db DB) (*User, error) { + return UserByID(ctx, db, qa.UserID) +} diff --git a/models/submissions.xo.go b/models/submissions.xo.go new file mode 100644 index 0000000..4e40522 --- /dev/null +++ b/models/submissions.xo.go @@ -0,0 +1,38 @@ +package models + +// Code generated by xo. DO NOT EDIT. + +import ( + "context" +) + +// Submissions represents a row from 'submissions'. +type Submissions struct { + ByUser int `json:"by_user"` // by_user + Correct bool `json:"correct"` // correct +} + +// QuestionSubmissions runs a custom query, returning results as Submissions. +func QuestionSubmissions(ctx context.Context, db DB, user_id, question_id string, question_part int) (*Submissions, error) { + // query + const sqlstr = `WITH attempts AS ( ` + + `SELECT * FROM question_attempt ` + + `WHERE user_id = $1 ` + + `AND question_id = $2 ` + + `AND question_part = $3 ` + + `) ` + + `SELECT ` + + `c.count AS by_user, ` + + `COALESCE(a.correct, FALSE) AS correct ` + + `FROM ( ` + + `SELECT COUNT(*) as count from attempts ` + + `) c ` + + `LEFT JOIN attempts a ON a.correct;` + // run + logf(sqlstr, user_id, question_id, question_part) + var s Submissions + if err := db.QueryRowContext(ctx, sqlstr, user_id, question_id, question_part).Scan(&s.ByUser, &s.Correct); err != nil { + return nil, logerror(err) + } + return &s, nil +} diff --git a/models/user.xo.go b/models/user.xo.go index 47d5979..3e939de 100644 --- a/models/user.xo.go +++ b/models/user.xo.go @@ -4,19 +4,16 @@ package models import ( "context" - "database/sql" ) // User represents a row from 'users'. type User struct { - ID string `json:"id"` // id - Name string `json:"name"` // name - Email string `json:"email"` // email - Picture string `json:"picture"` // picture - GradeLevel sql.NullInt64 `json:"grade_level"` // grade_level - Teacher bool `json:"teacher"` // teacher - Admin bool `json:"admin"` // admin - CreatedAt Time `json:"created_at"` // created_at + ID string `json:"id"` // id + Name string `json:"name"` // name + Email string `json:"email"` // email + Picture string `json:"picture"` // picture + Admin bool `json:"admin"` // admin + CreatedAt Time `json:"created_at"` // created_at // xo fields _exists, _deleted bool } @@ -42,13 +39,13 @@ func (u *User) Insert(ctx context.Context, db DB) error { } // insert (manual) const sqlstr = `INSERT INTO users (` + - `id, name, email, picture, grade_level, teacher, admin, created_at` + + `id, name, email, picture, admin, created_at` + `) VALUES (` + - `$1, $2, $3, $4, $5, $6, $7, $8` + + `$1, $2, $3, $4, $5, $6` + `)` // run - logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt) - if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt); err != nil { + logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Admin, u.CreatedAt) + if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Admin, u.CreatedAt); err != nil { return logerror(err) } // set exists @@ -66,11 +63,11 @@ func (u *User) Update(ctx context.Context, db DB) error { } // update with primary key const sqlstr = `UPDATE users SET ` + - `name = $1, email = $2, picture = $3, grade_level = $4, teacher = $5, admin = $6, created_at = $7 ` + - `WHERE id = $8` + `name = $1, email = $2, picture = $3, admin = $4, created_at = $5 ` + + `WHERE id = $6` // run - logf(sqlstr, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt, u.ID) - if _, err := db.ExecContext(ctx, sqlstr, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt, u.ID); err != nil { + logf(sqlstr, u.Name, u.Email, u.Picture, u.Admin, u.CreatedAt, u.ID) + if _, err := db.ExecContext(ctx, sqlstr, u.Name, u.Email, u.Picture, u.Admin, u.CreatedAt, u.ID); err != nil { return logerror(err) } return nil @@ -92,16 +89,16 @@ func (u *User) Upsert(ctx context.Context, db DB) error { } // upsert const sqlstr = `INSERT INTO users (` + - `id, name, email, picture, grade_level, teacher, admin, created_at` + + `id, name, email, picture, admin, created_at` + `) VALUES (` + - `$1, $2, $3, $4, $5, $6, $7, $8` + + `$1, $2, $3, $4, $5, $6` + `)` + ` ON CONFLICT (id) DO ` + `UPDATE SET ` + - `name = EXCLUDED.name, email = EXCLUDED.email, picture = EXCLUDED.picture, grade_level = EXCLUDED.grade_level, teacher = EXCLUDED.teacher, admin = EXCLUDED.admin, created_at = EXCLUDED.created_at ` + `name = EXCLUDED.name, email = EXCLUDED.email, picture = EXCLUDED.picture, admin = EXCLUDED.admin, created_at = EXCLUDED.created_at ` // run - logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt) - if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt); err != nil { + logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Admin, u.CreatedAt) + if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Admin, u.CreatedAt); err != nil { return logerror(err) } // set exists @@ -136,7 +133,7 @@ func (u *User) Delete(ctx context.Context, db DB) error { func UserByID(ctx context.Context, db DB, id string) (*User, error) { // query const sqlstr = `SELECT ` + - `id, name, email, picture, grade_level, teacher, admin, created_at ` + + `id, name, email, picture, admin, created_at ` + `FROM users ` + `WHERE id = $1` // run @@ -144,7 +141,7 @@ func UserByID(ctx context.Context, db DB, id string) (*User, error) { u := User{ _exists: true, } - if err := db.QueryRowContext(ctx, sqlstr, id).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil { + if err := db.QueryRowContext(ctx, sqlstr, id).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.Admin, &u.CreatedAt); err != nil { return nil, logerror(err) } return &u, nil @@ -156,7 +153,7 @@ func UserByID(ctx context.Context, db DB, id string) (*User, error) { func UserByEmail(ctx context.Context, db DB, email string) (*User, error) { // query const sqlstr = `SELECT ` + - `id, name, email, picture, grade_level, teacher, admin, created_at ` + + `id, name, email, picture, admin, created_at ` + `FROM users ` + `WHERE email = $1` // run @@ -164,7 +161,7 @@ func UserByEmail(ctx context.Context, db DB, email string) (*User, error) { u := User{ _exists: true, } - if err := db.QueryRowContext(ctx, sqlstr, email).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil { + if err := db.QueryRowContext(ctx, sqlstr, email).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.Admin, &u.CreatedAt); err != nil { return nil, logerror(err) } return &u, nil @@ -186,7 +183,7 @@ func Users(ctx context.Context, db DB) ([]*User, error) { for rows.Next() { var u User // scan - if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil { + if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.Admin, &u.CreatedAt); err != nil { return nil, logerror(err) } res = append(res, &u) @@ -208,7 +205,7 @@ func UserByToken(ctx context.Context, db DB, token string) (*User, error) { // run logf(sqlstr, token) var u User - if err := db.QueryRowContext(ctx, sqlstr, token).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil { + if err := db.QueryRowContext(ctx, sqlstr, token).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.Admin, &u.CreatedAt); err != nil { return nil, logerror(err) } return &u, nil diff --git a/models/userinfo.xo.go b/models/userinfo.xo.go new file mode 100644 index 0000000..1c255d3 --- /dev/null +++ b/models/userinfo.xo.go @@ -0,0 +1,152 @@ +package models + +// Code generated by xo. DO NOT EDIT. + +import ( + "context" +) + +// UserInfo represents a row from 'user_info'. +type UserInfo struct { + UserID string `json:"user_id"` // user_id + GradeLevel int `json:"grade_level"` // grade_level + SkillLevel int `json:"skill_level"` // skill_level + // xo fields + _exists, _deleted bool +} + +// Exists returns true when the UserInfo exists in the database. +func (ui *UserInfo) Exists() bool { + return ui._exists +} + +// Deleted returns true when the UserInfo has been marked for deletion from +// the database. +func (ui *UserInfo) Deleted() bool { + return ui._deleted +} + +// Insert inserts the UserInfo to the database. +func (ui *UserInfo) Insert(ctx context.Context, db DB) error { + switch { + case ui._exists: // already exists + return logerror(&ErrInsertFailed{ErrAlreadyExists}) + case ui._deleted: // deleted + return logerror(&ErrInsertFailed{ErrMarkedForDeletion}) + } + // insert (manual) + const sqlstr = `INSERT INTO user_info (` + + `user_id, grade_level, skill_level` + + `) VALUES (` + + `$1, $2, $3` + + `)` + // run + logf(sqlstr, ui.UserID, ui.GradeLevel, ui.SkillLevel) + if _, err := db.ExecContext(ctx, sqlstr, ui.UserID, ui.GradeLevel, ui.SkillLevel); err != nil { + return logerror(err) + } + // set exists + ui._exists = true + return nil +} + +// Update updates a UserInfo in the database. +func (ui *UserInfo) Update(ctx context.Context, db DB) error { + switch { + case !ui._exists: // doesn't exist + return logerror(&ErrUpdateFailed{ErrDoesNotExist}) + case ui._deleted: // deleted + return logerror(&ErrUpdateFailed{ErrMarkedForDeletion}) + } + // update with primary key + const sqlstr = `UPDATE user_info SET ` + + `grade_level = $1, skill_level = $2 ` + + `WHERE user_id = $3` + // run + logf(sqlstr, ui.GradeLevel, ui.SkillLevel, ui.UserID) + if _, err := db.ExecContext(ctx, sqlstr, ui.GradeLevel, ui.SkillLevel, ui.UserID); err != nil { + return logerror(err) + } + return nil +} + +// Save saves the UserInfo to the database. +func (ui *UserInfo) Save(ctx context.Context, db DB) error { + if ui.Exists() { + return ui.Update(ctx, db) + } + return ui.Insert(ctx, db) +} + +// Upsert performs an upsert for UserInfo. +func (ui *UserInfo) Upsert(ctx context.Context, db DB) error { + switch { + case ui._deleted: // deleted + return logerror(&ErrUpsertFailed{ErrMarkedForDeletion}) + } + // upsert + const sqlstr = `INSERT INTO user_info (` + + `user_id, grade_level, skill_level` + + `) VALUES (` + + `$1, $2, $3` + + `)` + + ` ON CONFLICT (user_id) DO ` + + `UPDATE SET ` + + `grade_level = EXCLUDED.grade_level, skill_level = EXCLUDED.skill_level ` + // run + logf(sqlstr, ui.UserID, ui.GradeLevel, ui.SkillLevel) + if _, err := db.ExecContext(ctx, sqlstr, ui.UserID, ui.GradeLevel, ui.SkillLevel); err != nil { + return logerror(err) + } + // set exists + ui._exists = true + return nil +} + +// Delete deletes the UserInfo from the database. +func (ui *UserInfo) Delete(ctx context.Context, db DB) error { + switch { + case !ui._exists: // doesn't exist + return nil + case ui._deleted: // deleted + return nil + } + // delete with single primary key + const sqlstr = `DELETE FROM user_info ` + + `WHERE user_id = $1` + // run + logf(sqlstr, ui.UserID) + if _, err := db.ExecContext(ctx, sqlstr, ui.UserID); err != nil { + return logerror(err) + } + // set deleted + ui._deleted = true + return nil +} + +// UserInfoByUserID retrieves a row from 'user_info' as a UserInfo. +// +// Generated from index 'sqlite_autoindex_user_info_1'. +func UserInfoByUserID(ctx context.Context, db DB, userID string) (*UserInfo, error) { + // query + const sqlstr = `SELECT ` + + `user_id, grade_level, skill_level ` + + `FROM user_info ` + + `WHERE user_id = $1` + // run + logf(sqlstr, userID) + ui := UserInfo{ + _exists: true, + } + if err := db.QueryRowContext(ctx, sqlstr, userID).Scan(&ui.UserID, &ui.GradeLevel, &ui.SkillLevel); err != nil { + return nil, logerror(err) + } + return &ui, nil +} + +// User returns the User associated with the UserInfo's (UserID). +// +// Generated from foreign key 'user_info_user_id_fkey'. +func (ui *UserInfo) User(ctx context.Context, db DB) (*User, error) { + return UserByID(ctx, db, ui.UserID) +} diff --git a/models/xo.xo.yaml b/models/xo.xo.yaml index 240bda5..a59ed92 100644 --- a/models/xo.xo.yaml +++ b/models/xo.xo.yaml @@ -4,6 +4,66 @@ schemas: - type: sqlite3 name: db.sqlite tables: + - type: table + name: question_attempt + columns: + - name: id + datatype: + type: integer + is_primary: true + is_sequence: true + - name: user_id + datatype: + type: text + - name: question_id + datatype: + type: text + - name: question_part + datatype: + type: integer + - name: correct + datatype: + type: boolean + - name: points_awarded + datatype: + type: integer + - name: input + datatype: + type: text + - name: code + datatype: + type: text + - name: submitted_at + datatype: + type: datetime + primary_keys: + - name: id + datatype: + type: integer + is_primary: true + is_sequence: true + indexes: + - name: question_attempt_id_pkey + fields: + - name: id + datatype: + type: integer + is_primary: true + is_sequence: true + is_unique: true + is_primary: true + foreign_keys: + - name: question_attempt_user_id_fkey + column: + - name: user_id + datatype: + type: text + ref_table: users + ref_column: + - name: id + datatype: + type: text + is_primary: true - type: table name: tokens columns: @@ -60,6 +120,47 @@ schemas: datatype: type: text is_primary: true + - type: table + name: user_info + columns: + - name: user_id + datatype: + type: text + is_primary: true + - name: grade_level + datatype: + type: integer + - name: skill_level + datatype: + type: integer + primary_keys: + - name: user_id + datatype: + type: text + is_primary: true + indexes: + - name: sqlite_autoindex_user_info_1 + fields: + - name: user_id + datatype: + type: text + is_primary: true + is_unique: true + is_primary: true + foreign_keys: + - name: user_info_user_id_fkey + column: + - name: user_id + datatype: + type: text + is_primary: true + ref_table: users + ref_column: + - name: id + datatype: + type: text + is_primary: true + manual: true - type: table name: users columns: @@ -76,13 +177,6 @@ schemas: - name: picture datatype: type: text - - name: grade_level - datatype: - type: integer - nullable: true - - name: teacher - datatype: - type: boolean - name: admin datatype: type: boolean diff --git a/question/data.go b/question/data.go new file mode 100644 index 0000000..e17a010 --- /dev/null +++ b/question/data.go @@ -0,0 +1,19 @@ +package question + +import "text/template" + +type PartData struct { + Completed bool + PointsWorth int + Solution string +} + +type Data struct { + QuestionID string + UserID string + Name string + Text *template.Template + Level Level + Part1 PartData + Part2 PartData +} diff --git a/question/q01/q01.go b/question/q01/q01.go index 0b287c2..b322344 100644 --- a/question/q01/q01.go +++ b/question/q01/q01.go @@ -5,17 +5,24 @@ import ( "fmt" "strconv" "strings" + "text/template" "github.com/hhhapz/codequest/models" "github.com/hhhapz/codequest/question" ) func init() { + t := template.New("directions") + var err error + t, err = t.Parse(q01Text) + if err != nil { + panic(err) + } question.Register( &question.Question{ ID: "directions", Name: "No Time for Directions!", - Text: q01Text, + Text: t, Level: question.Level1, Generate: func(u *models.User) string { return strings.Join(generate(u), "\n") diff --git a/question/q01/q01.md b/question/q01/q01.md index 6f9fbea..8df76c6 100644 --- a/question/q01/q01.md +++ b/question/q01/q01.md @@ -59,9 +59,9 @@ always be between 1 and 9, inclusive, steps. **How many steps away** is the key? -{{ if eq .Part 2 }} +{{ if .Part1.Completed -}} -**Congratulations! You got Part 1 correct. Your answer was `{{ .Answer1 }}`.** +**Congratulations! You got Part 1 correct. Your answer was `{{ .Part1.Solution }}`.** ## Part 2 @@ -88,4 +88,9 @@ The first location visit twice is `7` blocks away South. With these new instructions, to find the key, **how many steps away is the first location you visit twice?** -{{ end }} +{{ if .Part2.Completed -}} + +**Congratulations! You have completed both parts! The answer was `{{ .Part2.Solution }}`.** + +{{- end }} +{{- end }} diff --git a/question/q02/q02.go b/question/q02/q02.go index 7cbbba0..9fbd449 100644 --- a/question/q02/q02.go +++ b/question/q02/q02.go @@ -6,17 +6,24 @@ import ( "math/big" "strconv" "strings" + "text/template" "github.com/hhhapz/codequest/models" "github.com/hhhapz/codequest/question" ) func init() { + t := template.New("saturnalia") + var err error + t, err = t.Parse(q02Text) + if err != nil { + panic(err) + } question.Register( &question.Question{ ID: "saturnalia", Name: "Saturnalia's Problem", - Text: q02Text, + Text: t, Level: question.Level1, Generate: func(u *models.User) string { inp := generate(u) diff --git a/question/q02/q02.md b/question/q02/q02.md index 6e1d159..5a3b5f1 100644 --- a/question/q02/q02.md +++ b/question/q02/q02.md @@ -60,9 +60,9 @@ returns back at the same time after the leave? Hint: The numbers might get a bit large here! -{{ if eq .Part 2 }} +{{ if .Part1.Completed -}} -**Congratulations! You got Part 1 correct. Your answer was `{{ .Answer1 }}`.** +**Congratulations! You got Part 1 correct. Your answer was `{{ .Part1.Solution }}`.** ## Part 2 @@ -85,4 +85,9 @@ trips, and the last ship will complete `5` trips, for a total of `29` trips. With these new instructions, **how many steps total trips will all the ships complete?** -{{ end }} +{{ if .Part2.Completed -}} + +**Congratulations! You have completed both parts! The answer was `{{ .Part2.Solution }}`.** + +{{- end }} +{{- end }} diff --git a/question/q03.go b/question/q03.go deleted file mode 100644 index 9eac1f3..0000000 --- a/question/q03.go +++ /dev/null @@ -1 +0,0 @@ -package question diff --git a/question/question.go b/question/question.go index 83cb773..1f2e0d0 100644 --- a/question/question.go +++ b/question/question.go @@ -3,25 +3,19 @@ package question import ( "math/rand" "strconv" + "text/template" "github.com/hhhapz/codequest/models" ) -var bank Bank - -// Bank is a map of different questions registered. -// The key of the map is the ID of the question. -// -// A custom type was created for convenience for picking random questions based -// on difficulties, and for registration. -type Bank []*Question +var Questions []*Question func Register(q *Question) { - bank = append(bank, q) + Questions = append(Questions, q) } func QuestionByID(id string) *Question { - for _, q := range bank { + for _, q := range Questions { if q.ID == id { return q } @@ -29,15 +23,10 @@ func QuestionByID(id string) *Question { return nil } -func Questions(user *models.User, level Level) []*Question { - // TODO: player skill level should be used to determine which problems to return - return bank -} - type Question struct { ID string Name string - Text string + Text *template.Template Level Level Generate func(user *models.User) string diff --git a/sql/schema.sql b/sql/schema.sql index e3fd920..c9c3094 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -1,13 +1,23 @@ +PRAGMA journal_mode = WAL; +PRAGMA busy_timeout = 5000; +PRAGMA foreign_keys = ON; + CREATE TABLE users ( id TEXT PRIMARY KEY NOT NULL, -- supplied from google - not auto increment name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, picture TEXT NOT NULL, - grade_level INTEGER, + admin BOOLEAN NOT NULL, created_at DATETIME NOT NULL ); +CREATE TABLE user_info ( + user_id TEXT PRIMARY KEY NOT NULL REFERENCES users (id), + grade_level INTEGER NOT NULL, + skill_level INTEGER NOT NULL +); + CREATE TABLE tokens ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, token TEXT NOT NULL UNIQUE, @@ -16,3 +26,15 @@ CREATE TABLE tokens ( ); CREATE INDEX user_tokens ON tokens(user_id); + +CREATE TABLE question_attempt ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL REFERENCES users (id), + question_id TEXT NOT NULL, + question_part INTEGER NOT NULL, + correct BOOLEAN NOT NULL, + points_awarded INTEGER NOT NULL, + input TEXT NOT NULL, + code TEXT NOT NULL, + submitted_at DATETIME NOT NULL +);