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

Add stories moderation #120

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
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
140 changes: 140 additions & 0 deletions controller/stories/stories.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,146 @@ func HandleList(w http.ResponseWriter, r *http.Request) error {
return nil
}

func HandleListPublished(w http.ResponseWriter, r *http.Request) error {
err := auth.CheckPermissions(r, storypermissiongroups.List())
if err != nil {
logrus.Error(err)
return apierrors.ClientForbiddenError{
Message: fmt.Sprintf("Error listing published stories: %v", err),
}
}

// Get DB instance
db, err := database.GetDBFrom(r)
if err != nil {
logrus.Error(err)
return err
}

// Get group id from context
groupID, err := usergroups.GetGroupIDFrom(r)
if err != nil {
logrus.Error(err)
return err
}

stories, err := model.GetAllStoriesByStatus(db, groupID, model.Published)
if err != nil {
logrus.Error(err)
return err
}

controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
return nil
}

func HandleListPending(w http.ResponseWriter, r *http.Request) error {
err := auth.CheckPermissions(r, storypermissiongroups.Moderate())
if err != nil {
logrus.Error(err)
return apierrors.ClientForbiddenError{
Message: fmt.Sprintf("Error listing stories to be reviewed: %v", err),
}
}

// Get DB instance
db, err := database.GetDBFrom(r)
if err != nil {
logrus.Error(err)
return err
}

// Get group id from context
groupID, err := usergroups.GetGroupIDFrom(r)
if err != nil {
logrus.Error(err)
return err
}

stories, err := model.GetAllStoriesByStatus(db, groupID, model.Pending)
if err != nil {
logrus.Error(err)
return err
}

controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
return nil
}

func HandleListDraft(w http.ResponseWriter, r *http.Request) error {
err := auth.CheckPermissions(r, storypermissiongroups.List())
if err != nil {
logrus.Error(err)
return apierrors.ClientForbiddenError{
Message: fmt.Sprintf("Error listing drafts: %v", err),
}
}
userID, err := auth.GetUserIDFrom(r)
if err != nil {
logrus.Error(err)
return err
}
// Get DB instance
db, err := database.GetDBFrom(r)
if err != nil {
logrus.Error(err)
return err
}

// Get group id from context
groupID, err := usergroups.GetGroupIDFrom(r)
if err != nil {
logrus.Error(err)
return err
}

stories, err := model.GetAllAuthorStoriesByStatus(db, groupID, userID, model.Draft)
if err != nil {
logrus.Error(err)
return err
}

controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
return nil
}

func HandleListRejected(w http.ResponseWriter, r *http.Request) error {
err := auth.CheckPermissions(r, storypermissiongroups.List())
if err != nil {
logrus.Error(err)
return apierrors.ClientForbiddenError{
Message: fmt.Sprintf("Error listing rejected stories: %v", err),
}
}
userID, err := auth.GetUserIDFrom(r)
if err != nil {
logrus.Error(err)
return err
}
// Get DB instance
db, err := database.GetDBFrom(r)
if err != nil {
logrus.Error(err)
return err
}

// Get group id from context
groupID, err := usergroups.GetGroupIDFrom(r)
if err != nil {
logrus.Error(err)
return err
}

stories, err := model.GetAllAuthorStoriesByStatus(db, groupID, userID, model.Rejected)
if err != nil {
logrus.Error(err)
return err
}

controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
return nil
}

func HandleRead(w http.ResponseWriter, r *http.Request) error {
storyIDStr := chi.URLParam(r, "storyID")
storyID, err := strconv.Atoi(storyIDStr)
Expand Down
5 changes: 4 additions & 1 deletion internal/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ func MakeMiddlewareFrom(conf *config.Config) func(http.Handler) http.Handler {
// Skip auth in development mode
if conf.Environment == envutils.ENV_DEVELOPMENT {
return func(next http.Handler) http.Handler {
return next
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = injectUserIDToContext(r, 1)
next.ServeHTTP(w, r)
})
}
}

Expand Down
5 changes: 5 additions & 0 deletions internal/permissiongroups/stories/stories.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ func Read() permissions.PermissionGroup {
GetRolePermission(userpermissions.CanReadStories)
}

func Moderate() permissions.PermissionGroup {
return userpermissions.
GetRolePermission(userpermissions.CanModerateStories)
}

func Update(storyID uint) permissions.PermissionGroup {
return permissions.AnyOf{
Groups: []permissions.PermissionGroup{
Expand Down
9 changes: 5 additions & 4 deletions internal/permissions/users/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ const (
CanUpdateGroups Permission = "can_update_groups"
CanDeleteGroups Permission = "can_delete_groups"

CanCreateStories Permission = "can_create_stories"
CanReadStories Permission = "can_read_stories"
CanUpdateStories Permission = "can_update_stories"
CanDeleteStories Permission = "can_delete_stories"
CanCreateStories Permission = "can_create_stories"
CanReadStories Permission = "can_read_stories"
CanUpdateStories Permission = "can_update_stories"
CanDeleteStories Permission = "can_delete_stories"
CanModerateStories Permission = "can_moderate_stories"
)
1 change: 1 addition & 0 deletions internal/permissions/users/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func GetRolePermission(p Permission) *RolePermission {
case
// Additional permissions for moderators and administrators
CanUpdateStories,
CanModerateStories,
CanDeleteStories:
return &RolePermission{
Permission: p,
Expand Down
6 changes: 5 additions & 1 deletion internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
r.Use(usergroups.InjectUserGroupIntoContext)
r.Route("/stories", func(r chi.Router) {
r.Get("/", handleAPIError(stories.HandleList))
r.Get("/draft", handleAPIError(stories.HandleListDraft))
r.Get("/pending", handleAPIError(stories.HandleListPending))
r.Get("/published", handleAPIError(stories.HandleListPublished))
r.Get("/rejected", handleAPIError(stories.HandleListRejected))
Comment on lines +58 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering should be done via a query parameter, not separate routes.

r.Get("/{storyID}", handleAPIError(stories.HandleRead))
r.Put("/{storyID}", handleAPIError(stories.HandleUpdate))
r.Delete("/{storyID}", handleAPIError(stories.HandleDelete))
Expand All @@ -66,7 +70,7 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
r.Get("/{userID}", handleAPIError(users.HandleRead))
r.Delete("/{userID}", handleAPIError(users.HandleDelete))
r.Post("/", handleAPIError(users.HandleCreate))
r.Post("/batch", handleAPIError(usergroupscontroller.HandleBatchCreate))
r.Put("/batch", handleAPIError(usergroupscontroller.HandleBatchCreate))
})
})

Expand Down
9 changes: 9 additions & 0 deletions migrations/20240320000000-add_status.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- +migrate Up

ALTER TABLE stories
ADD COLUMN status INT;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any constraints we want to add?


-- +migrate Down

ALTER TABLE stories
DROP COLUMN status;
9 changes: 9 additions & 0 deletions migrations/20240320000001-add_status_message.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- +migrate Up

ALTER TABLE stories
ADD COLUMN status_message TEXT;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any constraints we want to add?


-- +migrate Down

ALTER TABLE stories
DROP COLUMN status_message;
106 changes: 98 additions & 8 deletions model/stories.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@ package model

import (
"github.com/source-academy/stories-backend/internal/database"
groupenums "github.com/source-academy/stories-backend/internal/enums/groups"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)

type StoryStatus int

const (
Draft StoryStatus = iota
Pending
Rejected
Published
)
Comment on lines +12 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enum should not be part of the model package and instead be a separate stories enum package.


type Story struct {
gorm.Model
AuthorID uint
Author User
GroupID *uint // null means this is a public story
Group Group
Title string
Content string
PinOrder *int // nil if not pinned
AuthorID uint
Author User
GroupID *uint // null means this is a public story
Group Group
Title string
Content string
PinOrder *int // nil if not pinned
Status StoryStatus
StatusMessage *string
}

// Passing nil to omit the filtering and get all stories
Expand All @@ -35,6 +47,71 @@ func GetAllStoriesInGroup(db *gorm.DB, groupID *uint) ([]Story, error) {
return stories, nil
}

func GetAllPublishedStories(db *gorm.DB, groupID *uint) ([]Story, error) {
var stories []Story
err := db.
Where("status = ?", int(Published)).
Where("group_id = ?", groupID).
Preload(clause.Associations).
// TODO: Abstract out the sorting logic
Order("pin_order ASC NULLS LAST, title ASC, content ASC").
Find(&stories).
Error
if err != nil {
return stories, database.HandleDBError(err, "story")
}
return stories, nil
}

func GetAllPendingStories(db *gorm.DB, groupID *uint) ([]Story, error) {
var stories []Story
err := db.
Where("status = ?", int(Pending)).
Where("group_id = ?", groupID).
Preload(clause.Associations).
// TODO: Abstract out the sorting logic
Order("pin_order ASC NULLS LAST, title ASC, content ASC").
Find(&stories).
Error
if err != nil {
return stories, database.HandleDBError(err, "story")
}
return stories, nil
}

func GetAllStoriesByStatus(db *gorm.DB, groupID *uint, status StoryStatus) ([]Story, error) {
var stories []Story
err := db.
Where("status = ?", int(status)).
Where("group_id = ?", groupID).
Preload(clause.Associations).
// TODO: Abstract out the sorting logic
Order("pin_order ASC NULLS LAST, title ASC, content ASC").
Find(&stories).
Error
if err != nil {
return stories, database.HandleDBError(err, "story")
}
return stories, nil
}

func GetAllAuthorStoriesByStatus(db *gorm.DB, groupID *uint, userID *int, status StoryStatus) ([]Story, error) {
var stories []Story
err := db.
Where("status = ?", int(status)).
Where("group_id = ?", groupID).
Where("author_id = ?", userID).
Preload(clause.Associations).
// TODO: Abstract out the sorting logic
Order("pin_order ASC NULLS LAST, title ASC, content ASC").
Find(&stories).
Error
if err != nil {
return stories, database.HandleDBError(err, "story")
}
return stories, nil
}
Comment on lines +50 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extensive code duplication. This can be easily combined.


func GetStoryByID(db *gorm.DB, id int) (Story, error) {
var story Story
err := db.
Expand All @@ -58,8 +135,21 @@ func (s *Story) create(tx *gorm.DB) *gorm.DB {
}

func CreateStory(db *gorm.DB, story *Story) error {
// Check author's role
role, _ := GetUserRoleByID(db, story.AuthorID)
// Based on the TestCreateStory, "can create without group" seems to be the desired behaviour
// No group means no userGroup, which means no role, so an error shouldn't be thrown
// Set story status based on author's role
if !groupenums.IsRoleGreaterThan(role, groupenums.RoleStandard) {
story.Status = Draft
} else {
story.Status = Published
}
err := db.Transaction(func(tx *gorm.DB) error {
return story.create(tx).Error
if err := tx.Create(story).Error; err != nil {
return err // Return the error directly
}
return nil
})
if err != nil {
return database.HandleDBError(err, "story")
Expand Down
14 changes: 14 additions & 0 deletions model/usergroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ func GetUserGroupByID(db *gorm.DB, userID uint, groupID uint) (UserGroup, error)
return userGroup, nil
}

func GetUserRoleByID(db *gorm.DB, userID uint) (groupenums.Role, error) {
var userGroup UserGroup

err := db.Model(&userGroup).
Where(UserGroup{UserID: userID}).
First(&userGroup).Error

if err != nil {
return userGroup.Role, database.HandleDBError(err, "userRole")
}

return userGroup.Role, nil
}

func CreateUserGroup(db *gorm.DB, userGroup *UserGroup) error {
err := db.Create(userGroup).Error
if err != nil {
Expand Down
Loading
Loading