From 2f05a2f8e74ba0e8a6469beaaf88b94b24d76319 Mon Sep 17 00:00:00 2001 From: nochill Date: Wed, 11 Oct 2023 16:31:52 +0700 Subject: [PATCH] add news events --- api/api.go | 6 ++ api/news_event.go | 71 +++++++++++++++ api/server.go | 6 +- .../000010_create_news_event_table.down.sql | 1 + .../000010_create_news_event_table.up.sql | 13 +++ db/mock/store.go | 29 +++++++ db/queries/news_events.sql | 8 ++ db/sqlc/locations.go | 6 +- db/sqlc/models.go | 14 +++ db/sqlc/news_events.go | 87 +++++++++++++++++++ db/sqlc/news_events.sql.go | 40 +++++++++ db/sqlc/querier.go | 1 + db/sqlc/store.go | 1 + 13 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 api/api.go create mode 100644 api/news_event.go create mode 100644 db/migrations/000010_create_news_event_table.down.sql create mode 100644 db/migrations/000010_create_news_event_table.up.sql create mode 100644 db/queries/news_events.sql create mode 100644 db/sqlc/news_events.go create mode 100644 db/sqlc/news_events.sql.go diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..4ee03eb --- /dev/null +++ b/api/api.go @@ -0,0 +1,6 @@ +package api + +type BaseGetListRequest struct { + Page int32 `form:"page" binding:"required,min=1"` + PageSize int32 `form:"page_size" binding:"required,min=5"` +} diff --git a/api/news_event.go b/api/news_event.go new file mode 100644 index 0000000..1211852 --- /dev/null +++ b/api/news_event.go @@ -0,0 +1,71 @@ +package api + +import ( + "database/sql" + "net/http" + + db "git.nochill.in/nochill/hiling_go/db/sqlc" + "github.com/gin-gonic/gin" +) + +type CreateNewsEventsReq struct { + Url string `json:"url" binding:"required,url"` + Title string `json:"title" binding:"required"` + Source string `json:"source"` + Description string `json:"description"` + SubmittedBy int32 `json:"submitted_by" binding:"required,numeric"` +} + +func (server *Server) createNews(ctx *gin.Context) { + var req CreateNewsEventsReq + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) + return + } + + err := server.Store.CreateNewsEvents(ctx, db.CreateNewsEventsParams{ + Title: req.Title, + Url: req.Url, + Source: req.Source, + Description: sql.NullString{Valid: len(req.Description) > 0, String: req.Description}, + SubmittedBy: req.SubmittedBy, + }) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save news/evnts")) + return + } + + ctx.Writer.WriteHeader(http.StatusCreated) +} + +type GetNewsEventsListReq struct { + BaseGetListRequest + Approved int8 `form:"is_with_approval"` +} + +func (server *Server) GetNewsEventsList(ctx *gin.Context) { + var req GetNewsEventsListReq + isWithApproval := "" + + if err := ctx.ShouldBindQuery(&req); err != nil { + ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) + return + } + if req.Approved > 0 { + isWithApproval = "WHERE approved_by IS NOT NULL" + } + + news, err := server.Store.GetNewsEventsList(ctx, db.GetNewsEventsListParams{ + Limit: req.PageSize, + Offset: (req.Page - 1) * req.PageSize, + IsWithApproved: isWithApproval, + }) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get news / events")) + return + } + + ctx.JSON(http.StatusOK, news) +} diff --git a/api/server.go b/api/server.go index 5cdd01f..dd51960 100644 --- a/api/server.go +++ b/api/server.go @@ -43,8 +43,6 @@ func (server *Server) getRoutes() { AllowMethods: []string{"POST", "PUT", "GET", "DELETE", "PATCH"}, })) - // router.Use(CORSMiddleware()) - router.POST("/user/signup", server.createUser) router.POST("/user/login", server.login) router.POST("/user/logout", server.logout) @@ -64,6 +62,9 @@ func (server *Server) getRoutes() { //IMAGES router.GET("/images/location", server.getAllImagesByLocation) + // NEWS / EVENTS + router.GET("/news-events", server.GetNewsEventsList) + // REQUIRE AUTH TOKEN authRoutes := router.Use(authMiddleware(server.TokenMaker)) authRoutes.POST("/review/location", server.createReview) @@ -72,6 +73,7 @@ func (server *Server) getRoutes() { authRoutes.GET("/user/profile", server.getUserStats) authRoutes.PATCH("/user/avatar", server.updateUserAvatar) authRoutes.DELETE("/user/avatar", server.removeAvatar) + authRoutes.POST("/news-events", server.createNews) server.Router = router } diff --git a/db/migrations/000010_create_news_event_table.down.sql b/db/migrations/000010_create_news_event_table.down.sql new file mode 100644 index 0000000..f05be0c --- /dev/null +++ b/db/migrations/000010_create_news_event_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS news_events; \ No newline at end of file diff --git a/db/migrations/000010_create_news_event_table.up.sql b/db/migrations/000010_create_news_event_table.up.sql new file mode 100644 index 0000000..3ac0129 --- /dev/null +++ b/db/migrations/000010_create_news_event_table.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE news_events ( + "id" serial primary key not null, + "title" varchar not null, + "url" varchar not null, + "source" varchar not null, + "thumbnail" varchar, + "description" text, + "is_deleted" boolean not null default(false), + "submitted_by" int references "users"("id") not null, + "approved_by" int references "users"("id"), + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +) \ No newline at end of file diff --git a/db/mock/store.go b/db/mock/store.go index cdc4362..a42d647 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -108,6 +108,20 @@ func (mr *MockStoreMockRecorder) CreateLocationTx(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLocationTx", reflect.TypeOf((*MockStore)(nil).CreateLocationTx), arg0, arg1) } +// CreateNewsEvents mocks base method. +func (m *MockStore) CreateNewsEvents(arg0 context.Context, arg1 db.CreateNewsEventsParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateNewsEvents", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateNewsEvents indicates an expected call of CreateNewsEvents. +func (mr *MockStoreMockRecorder) CreateNewsEvents(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNewsEvents", reflect.TypeOf((*MockStore)(nil).CreateNewsEvents), arg0, arg1) +} + // CreateReview mocks base method. func (m *MockStore) CreateReview(arg0 context.Context, arg1 db.CreateReviewParams) (db.Review, error) { m.ctrl.T.Helper() @@ -303,6 +317,21 @@ func (mr *MockStoreMockRecorder) GetLocationTag(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLocationTag", reflect.TypeOf((*MockStore)(nil).GetLocationTag), arg0, arg1) } +// GetNewsEventsList mocks base method. +func (m *MockStore) GetNewsEventsList(arg0 context.Context, arg1 db.GetNewsEventsListParams) ([]db.NewsEventRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNewsEventsList", arg0, arg1) + ret0, _ := ret[0].([]db.NewsEventRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNewsEventsList indicates an expected call of GetNewsEventsList. +func (mr *MockStoreMockRecorder) GetNewsEventsList(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewsEventsList", reflect.TypeOf((*MockStore)(nil).GetNewsEventsList), arg0, arg1) +} + // GetSession mocks base method. func (m *MockStore) GetSession(arg0 context.Context, arg1 int32) (db.UserSession, error) { m.ctrl.T.Helper() diff --git a/db/queries/news_events.sql b/db/queries/news_events.sql new file mode 100644 index 0000000..055b9bf --- /dev/null +++ b/db/queries/news_events.sql @@ -0,0 +1,8 @@ + -- name: CreateNewsEvents :exec +INSERT INTO news_events( + title, + url, + source, + description, + submitted_by +) VALUES ( $1, $2, $3, $4, $5); diff --git a/db/sqlc/locations.go b/db/sqlc/locations.go index 31f4b58..99d2f4e 100644 --- a/db/sqlc/locations.go +++ b/db/sqlc/locations.go @@ -46,9 +46,9 @@ func (q *Queries) GetTopListLocations(ctx context.Context, arg GetTopListLocatio FROM( SELECT *, - (SELECT 5 * 4 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) as critic_bayes, - (SELECT 50 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 50 + COALESCE(user_count, 0)) as user_bayes, - ((SELECT 50 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 50 + COALESCE(user_count, 0)) + (SELECT 5 * 4 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) ) / 2 as avg_bayes + (SELECT 5 * 5 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) as critic_bayes, + (SELECT 5 * 5 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 5 + COALESCE(user_count, 0)) as user_bayes, + ((SELECT 5 * 5 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 50 + COALESCE(user_count, 0)) + (SELECT 5 * 5 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) ) / 2 as avg_bayes FROM ( SELECT diff --git a/db/sqlc/models.go b/db/sqlc/models.go index b677b16..e3f787a 100644 --- a/db/sqlc/models.go +++ b/db/sqlc/models.go @@ -206,6 +206,20 @@ type LocationImage struct { UpdatedAt sql.NullTime `json:"updated_at"` } +type NewsEvent struct { + ID int32 `json:"id"` + Title string `json:"title"` + Url string `json:"url"` + Source string `json:"source"` + Thumbnail sql.NullString `json:"thumbnail"` + Description sql.NullString `json:"description"` + IsDeleted bool `json:"is_deleted"` + SubmittedBy int32 `json:"submitted_by"` + ApprovedBy sql.NullInt32 `json:"approved_by"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + type Province struct { ID int32 `json:"id"` ProvinceName string `json:"province_name"` diff --git a/db/sqlc/news_events.go b/db/sqlc/news_events.go new file mode 100644 index 0000000..947b3d8 --- /dev/null +++ b/db/sqlc/news_events.go @@ -0,0 +1,87 @@ +package db + +import ( + "context" + "fmt" + "time" +) + +type GetNewsEventsListParams struct { + Limit int32 + Offset int32 + IsWithApproved string +} + +type NewsEventRow struct { + ID int32 `json:"id"` + Title string `json:"title"` + Url string `json:"url"` + Source string `json:"source"` + Thumbnail string `json:"thumbnail"` + Description string `json:"description"` + IsDeleted bool `json:"is_deleted"` + SubmittedBy string `json:"submitted_by"` + ApprovedBy int32 `json:"approved_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (q *Queries) GetNewsEventsList(ctx context.Context, arg GetNewsEventsListParams) ([]NewsEventRow, error) { + getNewsEventsListQ := fmt.Sprintf(` + SELECT + n.id, + n.title, + n.url, + n.source, + COALESCE(n.thumbnail, '') as thumbnail, + COALESCE(n.description, '') as description, + n.is_deleted, + u.username as submitted_by, + COALESCE(n.approved_by, 0) as approved_by, + n.created_at, + n.updated_at + FROM news_events n + JOIN users u ON n.submitted_by = u.id + %s + ORDER BY n.created_at DESC + LIMIT $1 + OFFSET $2 + `, arg.IsWithApproved) + + rows, err := q.db.QueryContext(ctx, getNewsEventsListQ, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + + items := []NewsEventRow{} + + for rows.Next() { + var i NewsEventRow + if err := rows.Scan( + &i.ID, + &i.Title, + &i.Url, + &i.Source, + &i.Thumbnail, + &i.Description, + &i.IsDeleted, + &i.SubmittedBy, + &i.ApprovedBy, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + + items = append(items, i) + } + + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/db/sqlc/news_events.sql.go b/db/sqlc/news_events.sql.go new file mode 100644 index 0000000..86a231b --- /dev/null +++ b/db/sqlc/news_events.sql.go @@ -0,0 +1,40 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.20.0 +// source: news_events.sql + +package db + +import ( + "context" + "database/sql" +) + +const createNewsEvents = `-- name: CreateNewsEvents :exec +INSERT INTO news_events( + title, + url, + source, + description, + submitted_by +) VALUES ( $1, $2, $3, $4, $5) +` + +type CreateNewsEventsParams struct { + Title string `json:"title"` + Url string `json:"url"` + Source string `json:"source"` + Description sql.NullString `json:"description"` + SubmittedBy int32 `json:"submitted_by"` +} + +func (q *Queries) CreateNewsEvents(ctx context.Context, arg CreateNewsEventsParams) error { + _, err := q.db.ExecContext(ctx, createNewsEvents, + arg.Title, + arg.Url, + arg.Source, + arg.Description, + arg.SubmittedBy, + ) + return err +} diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index c3dd22d..dbef7c1 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -12,6 +12,7 @@ import ( type Querier interface { AddFollowUser(ctx context.Context, arg AddFollowUserParams) error CheckIfReviewExists(ctx context.Context, arg CheckIfReviewExistsParams) (int64, error) + CreateNewsEvents(ctx context.Context, arg CreateNewsEventsParams) error CreateSession(ctx context.Context, arg CreateSessionParams) (UserSession, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) GetCountImageByLocation(ctx context.Context, imageOf int32) (int64, error) diff --git a/db/sqlc/store.go b/db/sqlc/store.go index 701151c..0ba92a2 100644 --- a/db/sqlc/store.go +++ b/db/sqlc/store.go @@ -21,6 +21,7 @@ type Store interface { CreateLocation(ctx context.Context, arg CreateLocationParams) (int32, error) CreateImage(ctx context.Context, arg []CreateImageParams) error CreateLocationTx(ctx context.Context, arg CreateLocationTxParams) error + GetNewsEventsList(ctx context.Context, arg GetNewsEventsListParams) ([]NewsEventRow, error) } type SQLStore struct {