Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAC-19 Club CRUD #54

Merged
merged 8 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions backend/src/controllers/club.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package controllers

import (
"strconv"

"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/gofiber/fiber/v2"
)

type ClubController struct {
clubService services.ClubServiceInterface
}

func NewClubController(clubService services.ClubServiceInterface) *ClubController {
return &ClubController{clubService: clubService}
}

func (l *ClubController) GetAllClubs(c *fiber.Ctx) error {
defaultLimit := 10
defaultPage := 1

clubs, err := l.clubService.GetClubs(c.Query("limit", strconv.Itoa(defaultLimit)), c.Query("page", strconv.Itoa(defaultPage)))
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusOK).JSON(clubs)
}

func (l *ClubController) CreateClub(c *fiber.Ctx) error {
var clubBody models.CreateClubRequestBody
if err := c.BodyParser(&clubBody); err != nil {
return errors.FailedToParseRequestBody.FiberError(c)
}

club, err := l.clubService.CreateClub(clubBody)
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusCreated).JSON(club)
}

func (l *ClubController) GetClub(c *fiber.Ctx) error {
club, err := l.clubService.GetClub(c.Params("id"))
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusOK).JSON(club)
}

func (l *ClubController) UpdateClub(c *fiber.Ctx) error {
var clubBody models.UpdateClubRequestBody

if err := c.BodyParser(&clubBody); err != nil {
return errors.FailedToParseRequestBody.FiberError(c)
}

updatedClub, err := l.clubService.UpdateClub(c.Params("id"), clubBody)
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusOK).JSON(updatedClub)
}

func (l *ClubController) DeleteClub(c *fiber.Ctx) error {
err := l.clubService.DeleteClub(c.Params("id"))
if err != nil {
return err.FiberError(c)
}

return c.SendStatus(fiber.StatusNoContent)
}
13 changes: 10 additions & 3 deletions backend/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,16 @@ func createSuperUser(settings config.Settings, db *gorm.DB) error {
}

superClub := models.Club{
Name: "SAC",
Preview: "SAC",
Description: "SAC",
Name: "SAC",
Preview: "SAC",
Description: "SAC",
NumMembers: 0,
IsRecruiting: true,
RecruitmentCycle: models.RecruitmentCycle(models.Always),
RecruitmentType: models.Application,
ApplicationLink: "https://generatenu.com/apply",
Logo: "https://aws.amazon.com/s3",
Admin: []models.User{superUser},
}
if err := tx.Create(&superClub).Error; err != nil {
tx.Rollback()
Expand Down
38 changes: 38 additions & 0 deletions backend/src/errors/club.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package errors

import "github.com/gofiber/fiber/v2"

var (
FailedToValidateUserID = Error{
StatusCode: fiber.StatusBadRequest,
Message: "failed to validate user id",
}
FailedToValidateClub = Error{
StatusCode: fiber.StatusBadRequest,
Message: "failed to validate club",
}
FailedToCreateClub = Error{
StatusCode: fiber.StatusInternalServerError,
Message: "failed to create club",
}
FailedToGetClubs = Error{
StatusCode: fiber.StatusInternalServerError,
Message: "failed to get clubs",
}
FailedToGetClub = Error{
StatusCode: fiber.StatusInternalServerError,
Message: "failed to get club",
}
FailedToDeleteClub = Error{
StatusCode: fiber.StatusInternalServerError,
Message: "failed to delete club",
}
FailedToUpdateClub = Error{
StatusCode: fiber.StatusInternalServerError,
Message: "failed to update club",
}
ClubNotFound = Error{
StatusCode: fiber.StatusNotFound,
Message: "club not found",
}
)
41 changes: 33 additions & 8 deletions backend/src/models/club.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package models

import (
"time"

"github.com/google/uuid"
"gorm.io/gorm"
)

type RecruitmentCycle string
Expand All @@ -26,21 +25,22 @@ const (
type Club struct {
Model

SoftDeletedAt time.Time `gorm:"type:timestamptz;default:NULL" json:"-" validate:"-"`
SoftDeletedAt gorm.DeletedAt `gorm:"type:timestamptz;default:NULL" json:"-" validate:"-"`

Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"`
Preview string `gorm:"type:varchar(255)" json:"preview" validate:"required,max=255"`
Description string `gorm:"type:varchar(255)" json:"description" validate:"required,url,max=255"` // MongoDB URL
Description string `gorm:"type:varchar(255)" json:"description" validate:"required,http_url,mongo_url,max=255"` // MongoDB URL
NumMembers int `gorm:"type:int" json:"num_members" validate:"required,min=1"`
IsRecruiting bool `gorm:"type:bool;default:false" json:"is_recruiting" validate:"required"`
RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255"`
RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255"`
ApplicationLink string `gorm:"type:varchar(255);default:NULL" json:"application_link" validate:"required,max=255"`
Logo string `gorm:"type:varchar(255);default:NULL" json:"logo" validate:"url,max=255"` // S3 URL
RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"`
RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"`
ApplicationLink string `gorm:"type:varchar(255);default:NULL" json:"application_link" validate:"required,max=255,http_url"`
Logo string `gorm:"type:varchar(255);default:NULL" json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL

Parent *uuid.UUID `gorm:"foreignKey:Parent" json:"-" validate:"uuid4"`
Tag []Tag `gorm:"many2many:club_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
// User
Admin []User `gorm:"many2many:user_club_admins;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"required"`
Member []User `gorm:"many2many:user_club_members;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"required"`
Follower []User `gorm:"many2many:user_club_followers;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
IntendedApplicant []User `gorm:"many2many:user_club_intended_applicants;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Expand All @@ -51,3 +51,28 @@ type Club struct {
Event []Event `gorm:"many2many:club_events;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Notifcation []Notification `gorm:"polymorphic:Reference;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
}

type CreateClubRequestBody struct {
UserID uuid.UUID `json:"user_id" validate:"required,uuid4"`
Name string `json:"name" validate:"required,max=255"`
Preview string `json:"preview" validate:"required,max=255"`
Description string `json:"description" validate:"required,http_url,mongo_url,max=255"` // MongoDB URL
NumMembers int `json:"num_members" validate:"required,min=1"`
IsRecruiting bool `json:"is_recruiting" validate:"required"`
RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"`
RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"`
ApplicationLink string `json:"application_link" validate:"required,max=255,http_url"`
Logo string `json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL
}

type UpdateClubRequestBody struct {
Name string `json:"name" validate:"omitempty,max=255"`
Preview string `json:"preview" validate:"omitempty,max=255"`
Description string `json:"description" validate:"omitempty,http_url,mongo_url,max=255"` // MongoDB URL
NumMembers int `json:"num_members" validate:"omitempty,min=1"`
IsRecruiting bool `json:"is_recruiting" validate:"omitempty"`
RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"`
RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"`
ApplicationLink string `json:"application_link" validate:"omitempty,required,max=255,http_url"`
Logo string `json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL
}
2 changes: 1 addition & 1 deletion backend/src/models/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Contact struct {
Model

Type Media `gorm:"type:varchar(255)" json:"type" validate:"required,max=255"`
Content string `gorm:"type:varchar(255)" json:"content" validate:"required,url,max=255"` // media URL
Content string `gorm:"type:varchar(255)" json:"content" validate:"required,http_url,max=255"` // media URL

ClubID uuid.UUID `gorm:"foreignKey:ClubID" json:"-" validate:"uuid4"`
}
2 changes: 1 addition & 1 deletion backend/src/models/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Notification struct {
Title string `gorm:"type:varchar(255)" json:"title" validate:"required,max=255"`
Content string `gorm:"type:varchar(255)" json:"content" validate:"required,max=255"`
DeepLink string `gorm:"type:varchar(255)" json:"deep_link" validate:"required,max=255"`
Icon string `gorm:"type:varchar(255)" json:"icon" validate:"required,url,max=255"` // S3 URL
Icon string `gorm:"type:varchar(255)" json:"icon" validate:"required,http_url,max=255"` // S3 URL

ReferenceID uuid.UUID `gorm:"type:int" json:"-" validate:"uuid4"`
ReferenceType NotificationType `gorm:"type:varchar(255)" json:"-" validate:"max=255"`
Expand Down
2 changes: 1 addition & 1 deletion backend/src/models/point_of_contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type PointOfContact struct {

Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"`
Email string `gorm:"type:varchar(255)" json:"email" validate:"required,email,max=255"`
Photo string `gorm:"type:varchar(255);default:NULL" json:"photo" validate:"url,max=255"` // S3 URL, fallback to default logo if null
Photo string `gorm:"type:varchar(255);default:NULL" json:"photo" validate:"http_url,max=255"` // S3 URL, fallback to default logo if null
Position string `gorm:"type:varchar(255);" json:"position" validate:"required,max=255"`

ClubID uuid.UUID `gorm:"foreignKey:ClubID" json:"-" validate:"uuid4"`
Expand Down
13 changes: 13 additions & 0 deletions backend/src/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func Init(db *gorm.DB) *fiber.App {
apiv1 := app.Group("/api/v1")

userRoutes(apiv1, &services.UserService{DB: db, Validate: validate})
clubRoutes(apiv1, &services.ClubService{DB: db, Validate: validate})
categoryRoutes(apiv1, &services.CategoryService{DB: db, Validate: validate})
tagRoutes(apiv1, &services.TagService{DB: db, Validate: validate})

Expand Down Expand Up @@ -75,6 +76,18 @@ func userRoutes(router fiber.Router, userService services.UserServiceInterface)
users.Delete("/:id", userController.DeleteUser)
}

func clubRoutes(router fiber.Router, clubService services.ClubServiceInterface) {
clubController := controllers.NewClubController(clubService)

clubs := router.Group("/clubs")

clubs.Get("/", clubController.GetAllClubs)
clubs.Post("/", clubController.CreateClub)
clubs.Get("/:id", clubController.GetClub)
clubs.Patch("/:id", clubController.UpdateClub)
clubs.Delete("/:id", clubController.DeleteClub)
}

func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) {
categoryController := controllers.NewCategoryController(categoryService)

Expand Down
91 changes: 91 additions & 0 deletions backend/src/services/club.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package services

import (
"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
"github.com/GenerateNU/sac/backend/src/transactions"
"github.com/GenerateNU/sac/backend/src/utilities"
"github.com/go-playground/validator/v10"

"gorm.io/gorm"
)

type ClubServiceInterface interface {
GetClubs(limit string, page string) ([]models.Club, *errors.Error)
GetClub(id string) (*models.Club, *errors.Error)
CreateClub(clubBody models.CreateClubRequestBody) (*models.Club, *errors.Error)
UpdateClub(id string, clubBody models.UpdateClubRequestBody) (*models.Club, *errors.Error)
DeleteClub(id string) *errors.Error
}

type ClubService struct {
DB *gorm.DB
Validate *validator.Validate
}

func (c *ClubService) GetClubs(limit string, page string) ([]models.Club, *errors.Error) {
limitAsInt, err := utilities.ValidateNonNegative(limit)

if err != nil {
return nil, &errors.FailedToValidateLimit
}

pageAsInt, err := utilities.ValidateNonNegative(page)

if err != nil {
return nil, &errors.FailedToValidatePage
}

offset := (*pageAsInt - 1) * *limitAsInt

return transactions.GetClubs(c.DB, *limitAsInt, offset)
}

func (c *ClubService) CreateClub(clubBody models.CreateClubRequestBody) (*models.Club, *errors.Error) {
if err := c.Validate.Struct(clubBody); err != nil {
return nil, &errors.FailedToValidateClub
}

club, err := utilities.MapRequestToModel(clubBody, &models.Club{})
if err != nil {
return nil, &errors.FailedToMapRequestToModel
}

return transactions.CreateClub(c.DB, clubBody.UserID, *club)
}

func (c *ClubService) GetClub(id string) (*models.Club, *errors.Error) {
idAsUUID, err := utilities.ValidateID(id)
if err != nil {
return nil, &errors.FailedToValidateID
}

return transactions.GetClub(c.DB, *idAsUUID)
}

func (c *ClubService) UpdateClub(id string, clubBody models.UpdateClubRequestBody) (*models.Club, *errors.Error) {
idAsUUID, idErr := utilities.ValidateID(id)
if idErr != nil {
return nil, idErr
}

if err := c.Validate.Struct(clubBody); err != nil {
return nil, &errors.FailedToValidateClub
}

club, err := utilities.MapRequestToModel(clubBody, &models.Club{})
if err != nil {
return nil, &errors.FailedToMapRequestToModel
}

return transactions.UpdateClub(c.DB, *idAsUUID, *club)
}

func (c *ClubService) DeleteClub(id string) *errors.Error {
idAsUUID, err := utilities.ValidateID(id)
if err != nil {
return &errors.FailedToValidateID
}

return transactions.DeleteClub(c.DB, *idAsUUID)
}
Loading
Loading