Skip to content

Commit

Permalink
Merge pull request #46 from mattrltrent/watched_schools
Browse files Browse the repository at this point in the history
Adds watched schools
  • Loading branch information
mattrltrent authored Jun 22, 2023
2 parents da51312 + 7a884d9 commit 6fdcfe9
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 9 deletions.
15 changes: 9 additions & 6 deletions db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,23 @@ func (Faculty) TableName() string {
}

type User struct {
ID string `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
ID string `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
Email string
YearOfStudy uint8
FacultyID uint
SchoolID uint
ModID uint
}

// ! Very important some fields are NOT serialized (json:"-")
type SchoolFollow struct {
ID uint
UserID uint
SchoolID uint
ID uint `gorm:"primary_key;column:id" json:"-"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"-"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"-"`
UserID string `gorm:"column:user_id" json:"-"`
SchoolID uint
}

type Post struct {
Expand Down
3 changes: 2 additions & 1 deletion features/schools/get_schools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package schools

import (
"confesi/db"
"confesi/lib/fire"
"log"
"net/http"
"net/http/httptest"
Expand All @@ -11,7 +12,7 @@ import (
"github.com/stretchr/testify/assert"
)

var h handler = handler{db.New()}
var h handler = handler{db.New(), fire.New()}

func routerSetup() *gin.Engine {
mux := gin.Default()
Expand Down
49 changes: 49 additions & 0 deletions features/schools/get_watched_schools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package schools

import (
"confesi/lib/response"
"confesi/lib/utils"
"net/http"

"firebase.google.com/go/auth"
"github.com/gin-gonic/gin"
)

// value that gets sent back to client for each of their watched schools
type schoolResult struct {
ID uint `json:"id"`
Name string `json:"name"`
Abbr string `json:"abbr"`
Lat string `json:"lat"`
Lon string `json:"lon"`
Domain string `json:"domain"`
}

func (h *handler) getWatchedSchools(c *gin.Context, token *auth.Token) ([]schoolResult, error) {
schools := []schoolResult{}
err := h.DB.
Table("school_follows").
Select("schools.id as id, schools.name, schools.abbr, schools.lat, schools.lon, schools.domain").
Joins("JOIN schools ON school_follows.school_id = schools.id").
Find(&schools).Error
if err != nil {
return nil, serverError
}
return schools, nil
}

// TODO: should this be limited to only N schools? Paginated? Or
// TODO: will this be cached locally so we'd want to get everything?
func (h *handler) handleGetWatchedSchools(c *gin.Context) {
token, err := utils.UserTokenFromContext(c)
if err != nil {
response.New(http.StatusInternalServerError).Err(serverError.Error()).Send(c)
return
}
schools, err := h.getWatchedSchools(c, token)
if err != nil {
response.New(http.StatusInternalServerError).Err(err.Error()).Send(c)
return
}
response.New(http.StatusOK).Val(schools).Send(c)
}
31 changes: 31 additions & 0 deletions features/schools/requests.http
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
### Get schools

GET http://127.0.0.1:8080/api/v1/schools?offset=1&limit=10&school=uvic
Content-Type: application/json
X-AppCheck-Token: kXfeSRgYTnoUztu6MO8FndqiRayoBaJqyDKQmoqvX3V9sZVlep/cm7cP!mgd-B9H
Expand All @@ -9,3 +11,32 @@ X-AppCheck-Token: kXfeSRgYTnoUztu6MO8FndqiRayoBaJqyDKQmoqvX3V9sZVlep/cm7cP!mgd-B
GET http://127.0.0.1:8080/api/v1/schools?offset=1&limit=10
Content-Type: application/json
X-AppCheck-Token: kXfeSRgYTnoUztu6MO8FndqiRayoBaJqyDKQmoqvX3V9sZVlep/cm7cP!mgd-B9H

### Watch a school

POST http://127.0.0.1:8080/api/v1/schools/watch
Content-Type: application/json
X-AppCheck-Token: kXfeSRgYTnoUztu6MO8FndqiRayoBaJqyDKQmoqvX3V9sZVlep/cm7cP!mgd-B9H
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjY3YmFiYWFiYTEwNWFkZDZiM2ZiYjlmZjNmZjVmZTNkY2E0Y2VkYTEiLCJ0eXAiOiJKV1QifQ.eyJwcm9maWxlX2NyZWF0ZWQiOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9jb25mZXNpLXNlcnZlci1kZXYiLCJhdWQiOiJjb25mZXNpLXNlcnZlci1kZXYiLCJhdXRoX3RpbWUiOjE2ODY5MTA3MTQsInVzZXJfaWQiOiJIOU11OG9hOHI1WmpaREh5OXdXNkF4SElvaksyIiwic3ViIjoiSDlNdThvYThyNVpqWkRIeTl3VzZBeEhJb2pLMiIsImlhdCI6MTY4NjkxMDcxNCwiZXhwIjoxNjg2OTE0MzE0LCJlbWFpbCI6InVzZXI3QHV2aWMuY2EiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsidXNlcjdAdXZpYy5jYSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.Fz12669YpWc2GMWvkC1axQ2fOWVfHIx7QT1kiwow5zYKD3t5J0dUpvB9SK7KPJkigwzvWA7F1grKR9PnQRclGwE73-oZxBC3wfFKV-z0nfAZk26ApWWCDdZ_OPLETAzwk7SkrRkOPbpUD1Pvq7vOdBfskfUY1FFPWBRIg4Wwzc6Bdlkh6K2KWB0VBfJtRBGbdNVPiNZy-HTf6LJkMKOFksUMkMicr7BX7dmOnO729v4bYCRC0yX0U9MM9aFTfP6shq-ldYs6z3Lr8Eg4VXqFMIaVKpPw_1bD6X6G-PG1mVmMsTRhr8D5HZI7rBK57CW353YVGsmq_TMWfHMWIa8zPw

{
"school_id": 1
}

### Unwatch a school

DELETE http://127.0.0.1:8080/api/v1/schools/unwatch
Content-Type: application/json
X-AppCheck-Token: kXfeSRgYTnoUztu6MO8FndqiRayoBaJqyDKQmoqvX3V9sZVlep/cm7cP!mgd-B9H
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjY3YmFiYWFiYTEwNWFkZDZiM2ZiYjlmZjNmZjVmZTNkY2E0Y2VkYTEiLCJ0eXAiOiJKV1QifQ.eyJwcm9maWxlX2NyZWF0ZWQiOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9jb25mZXNpLXNlcnZlci1kZXYiLCJhdWQiOiJjb25mZXNpLXNlcnZlci1kZXYiLCJhdXRoX3RpbWUiOjE2ODY5MTA3MTQsInVzZXJfaWQiOiJIOU11OG9hOHI1WmpaREh5OXdXNkF4SElvaksyIiwic3ViIjoiSDlNdThvYThyNVpqWkRIeTl3VzZBeEhJb2pLMiIsImlhdCI6MTY4NjkxMDcxNCwiZXhwIjoxNjg2OTE0MzE0LCJlbWFpbCI6InVzZXI3QHV2aWMuY2EiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsidXNlcjdAdXZpYy5jYSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.Fz12669YpWc2GMWvkC1axQ2fOWVfHIx7QT1kiwow5zYKD3t5J0dUpvB9SK7KPJkigwzvWA7F1grKR9PnQRclGwE73-oZxBC3wfFKV-z0nfAZk26ApWWCDdZ_OPLETAzwk7SkrRkOPbpUD1Pvq7vOdBfskfUY1FFPWBRIg4Wwzc6Bdlkh6K2KWB0VBfJtRBGbdNVPiNZy-HTf6LJkMKOFksUMkMicr7BX7dmOnO729v4bYCRC0yX0U9MM9aFTfP6shq-ldYs6z3Lr8Eg4VXqFMIaVKpPw_1bD6X6G-PG1mVmMsTRhr8D5HZI7rBK57CW353YVGsmq_TMWfHMWIa8zPw

{
"school_id": 1
}

### Get all watches schools for a user

GET http://127.0.0.1:8080/api/v1/schools/watched
Content-Type: application/json
X-AppCheck-Token: kXfeSRgYTnoUztu6MO8FndqiRayoBaJqyDKQmoqvX3V9sZVlep/cm7cP!mgd-B9H
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjY3YmFiYWFiYTEwNWFkZDZiM2ZiYjlmZjNmZjVmZTNkY2E0Y2VkYTEiLCJ0eXAiOiJKV1QifQ.eyJwcm9maWxlX2NyZWF0ZWQiOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9jb25mZXNpLXNlcnZlci1kZXYiLCJhdWQiOiJjb25mZXNpLXNlcnZlci1kZXYiLCJhdXRoX3RpbWUiOjE2ODY5MTU0ODQsInVzZXJfaWQiOiJIOU11OG9hOHI1WmpaREh5OXdXNkF4SElvaksyIiwic3ViIjoiSDlNdThvYThyNVpqWkRIeTl3VzZBeEhJb2pLMiIsImlhdCI6MTY4NjkxNTQ4NCwiZXhwIjoxNjg2OTE5MDg0LCJlbWFpbCI6InVzZXI3QHV2aWMuY2EiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsidXNlcjdAdXZpYy5jYSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.Pqynq1gT5Kb77CQ8CGMO0saA2mvaCAJrvIuDNrPdH1FJZp2Id8ZPrWEmdSZ5iJPGXnajEqklJZjMhkwuHLk8_v_ndYFTqeYPFLTKdNTVsStF7LmJ0Vt7msN2DwFvk1v1aTewUXSAiyCZssl2OUqzZs-xWJbm_VGkgM7MLsLi1Lpk8gcTwbqhmhXQzKXPLt7WwnNbIiOkNCA1pRa3KNmyA7dO43mLM1J5lOIgIZ8bx25hWB9b4Bmqpf2bk6kIaQRWoX7b2V00LrtWF3ldbbKcJlcaLWQm9SgwHVp4O74zq8MS3pSvENnJGFgvP5vN-F86X8aRIdiE565aLGPTwiAYvQ
22 changes: 20 additions & 2 deletions features/schools/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,35 @@ package schools

import (
"confesi/db"
"confesi/lib/fire"
"confesi/middleware"
"errors"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

var (
serverError = errors.New("server error")
invalidId = errors.New("invalid id")
)

type handler struct {
*gorm.DB
fb *fire.FirebaseApp
}

func Router(r *gin.RouterGroup) {
h := handler{db.New()}
//
h := handler{db.New(), fire.New()}

r.GET("/", h.getSchools)

// protected route
protectedRoutes := r.Group("")
protectedRoutes.Use(func(c *gin.Context) {
middleware.UsersOnly(c, h.fb.AuthClient, middleware.RegisteredFbUsers, []string{})
})
protectedRoutes.POST("/watch", h.handleWatchSchool)
protectedRoutes.DELETE("/unwatch", h.handleUnwatchSchool)
protectedRoutes.GET("/watched", h.handleGetWatchedSchools)
}
45 changes: 45 additions & 0 deletions features/schools/unwatch_school.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package schools

import (
"confesi/db"
"confesi/lib/response"
"confesi/lib/utils"
"confesi/lib/validation"
"net/http"

"firebase.google.com/go/auth"
"github.com/gin-gonic/gin"
)

func (h *handler) unwatchSchool(c *gin.Context, token *auth.Token, req validation.WatchSchool) error {
school := db.SchoolFollow{
UserID: token.UID,
SchoolID: req.SchoolID,
}
err := h.DB.Delete(&school, "user_id = ? AND school_id = ?", school.UserID, school.SchoolID).Error
if err != nil {
return serverError
}
return nil
}

func (h *handler) handleUnwatchSchool(c *gin.Context) {
// extract request
var req validation.WatchSchool

err := utils.New(c).Validate(&req)
if err != nil {
return
}

token, err := utils.UserTokenFromContext(c)
if err != nil {
response.New(http.StatusInternalServerError).Err(serverError.Error()).Send(c)
return
}
err = h.unwatchSchool(c, token, req)
if err != nil {
response.New(http.StatusInternalServerError).Err(err.Error()).Send(c)
}
response.New(http.StatusOK).Send(c)
}
68 changes: 68 additions & 0 deletions features/schools/watch_school.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package schools

import (
"confesi/db"
"confesi/lib/response"
"confesi/lib/utils"
"confesi/lib/validation"
"errors"
"net/http"

"firebase.google.com/go/auth"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgconn"
)

func (h *handler) watchSchool(c *gin.Context, token *auth.Token, req validation.WatchSchool) error {
school := db.SchoolFollow{
UserID: token.UID,
SchoolID: req.SchoolID,
}
err := h.DB.Create(&school).Error
if err != nil {
var pgErr *pgconn.PgError
// Gorm doesn't properly handle duplicate errors: https://github.com/go-gorm/gorm/issues/4037
if ok := errors.As(err, &pgErr); !ok {
// if it's not a PostgreSQL error, return a generic server error
return serverError
}
switch pgErr.Code {
case "23505": // duplicate key value violates unique constraint
return nil // just let the user know it's been watched, if it's already there
case "23503": // foreign key constraint violation
return invalidId // aka, you provided an school id to try watching
default:
// some other postgreSQL error
return serverError
}
}
return nil
}

func (h *handler) handleWatchSchool(c *gin.Context) {
// extract request
var req validation.WatchSchool

err := utils.New(c).Validate(&req)
if err != nil {
return
}

token, err := utils.UserTokenFromContext(c)
if err != nil {
response.New(http.StatusInternalServerError).Err(serverError.Error()).Send(c)
return
}
err = h.watchSchool(c, token, req)
if err != nil {
// switch over err
switch err {
case invalidId:
response.New(http.StatusBadRequest).Err(err.Error()).Send(c)
default:
response.New(http.StatusInternalServerError).Err(err.Error()).Send(c)
}
return
}
response.New(http.StatusCreated).Send(c)
}
5 changes: 5 additions & 0 deletions lib/validation/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ type VoteDetail struct {
ContentType string `json:"content_type" validate:"required,oneof=post comment"`
}

type WatchSchool struct {
// [required] school id to watch
SchoolID uint `json:"school_id" validate:"required"`
}

type UserStanding struct {
// [required] user standing must be one of "limited", "banned", or "enabled"
Standing string `json:"standing" validate:"required,oneof=limited banned enabled"`
Expand Down
7 changes: 7 additions & 0 deletions migrations/000004_school_follows.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
BEGIN;

ALTER TABLE school_follows
DROP COLUMN created_at,
DROP COLUMN updated_at;

END;
7 changes: 7 additions & 0 deletions migrations/000004_school_follows.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
BEGIN;

ALTER TABLE school_follows
ADD COLUMN created_at TIMESTAMPTZ NOT NULL,
ADD COLUMN updated_at TIMESTAMPTZ;

END;

0 comments on commit 6fdcfe9

Please sign in to comment.