Skip to content

Commit

Permalink
feat: remove course favorites, add program favs
Browse files Browse the repository at this point in the history
  • Loading branch information
PThorpe92 committed Nov 19, 2024
1 parent 89c1449 commit eee02b1
Show file tree
Hide file tree
Showing 27 changed files with 147 additions and 269 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- +goose Up
-- +goose StatementBegin
DROP TABLE IF EXISTS favorites CASCADE;
CREATE TABLE public.program_favorites (
id SERIAL PRIMARY KEY,
program_id INT NOT NULL,
user_id INT NOT NULL,
FOREIGN KEY (program_id) REFERENCES programs(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX program_favorites_program_id_index ON program_favorites USING btree (program_id);
CREATE INDEX program_favorites_user_id_index ON program_favorites USING btree (user_id);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
CREATE TABLE public.favorites (
id SERIAL NOT NULL PRIMARY KEY,
user_id integer,
course_id integer,
FOREIGN KEY (user_id) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES public.courses(id) ON UPDATE CASCADE ON DELETE CASCADE
);
DROP TABLE IF EXISTS program_favorites CASCADE;
-- +goose StatementEnd
2 changes: 1 addition & 1 deletion backend/src/database/DB.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func MigrateTesting(db *gorm.DB) {
&models.Outcome{},
&models.Activity{},
&models.OidcClient{},
&models.UserFavorite{},
&models.ProgramFavorite{},
&models.Facility{},
&models.OpenContentProvider{},
&models.CronJob{},
Expand Down
26 changes: 21 additions & 5 deletions backend/src/database/programs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func (db *DB) GetProgramByID(id int) (*models.Program, error) {
return content, nil
}

func (db *DB) GetProgram(page, perPage int, tags []string, search string) (int64, []models.Program, error) {
func (db *DB) GetProgram(page, perPage int, tags []string, search string, userId uint) (int64, []models.Program, error) {
content := []models.Program{}
total := int64(0)
if len(tags) > 0 {
Expand All @@ -26,23 +26,39 @@ func (db *DB) GetProgram(page, perPage int, tags []string, search string) (int64
}
}

tx := db.Model(&models.Program{}).Preload("Tags").Preload("Facilities").Find(&content, "id IN (?)", query).Count(&total)
tx := db.Model(&models.Program{}).Preload("Tags").Preload("Facilities").Preload("Favorites", "user_id = ?", userId).Find(&content, "id IN (?)", query)
if search != "" {
tx = tx.Where("name LIKE ?", "%"+search+"%")
}
if err := tx.Count(&total).Error; err != nil {
return 0, nil, newGetRecordsDBError(err, "programs")
}
if err := tx.Limit(perPage).Offset((page - 1) * perPage).Error; err != nil {
return 0, nil, newGetRecordsDBError(err, "programs")
}
} else {
tx := db.Model(&models.Program{}).Preload("Tags").Preload("Facilities")
tx := db.Model(&models.Program{}).
Preload("Tags").
Preload("Facilities").
Preload("Favorites", "user_id = ?", userId)
if search != "" {
tx = tx.Where("name LIKE ?", "%"+search+"%").Count(&total)
}
if err := tx.Find(&content).Limit(perPage).Offset((page - 1) * perPage).Error; err != nil {
if err := tx.Count(&total).Error; err != nil {
return 0, nil, newGetRecordsDBError(err, "programs")
}
if err := tx.Limit(perPage).Offset((page - 1) * perPage).Find(&content).Error; err != nil {
return 0, nil, newGetRecordsDBError(err, "programs")
}
}
return total, content, nil
programs := iterMap(func(prog models.Program) models.Program {
if len(prog.Favorites) > 0 {
prog.IsFavorited = true
return prog
}
return prog
}, content)
return total, programs, nil
}

func (db *DB) CreateProgram(content *models.Program) error {
Expand Down
12 changes: 2 additions & 10 deletions backend/src/database/user_catalogue.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@ type UserCatalogueJoin struct {
ExternalURL string `json:"external_url"`
CourseType string `json:"course_type"`
Description string `json:"description"`
IsFavorited bool `json:"is_favorited"`
OutcomeTypes string `json:"outcome_types"`
}

func (db *DB) GetUserCatalogue(userId int, tags []string, search, order string) ([]UserCatalogueJoin, error) {
catalogue := []UserCatalogueJoin{}
tx := db.Table("courses c").
Select("c.id as course_id, c.thumbnail_url, c.name as course_name, pp.name as provider_name, c.external_url, c.type as course_type, c.description, c.outcome_types, f.user_id IS NOT NULL as is_favorited").
Select("c.id as course_id, c.thumbnail_url, c.name as course_name, pp.name as provider_name, c.external_url, c.type as course_type, c.description, c.outcome_types").
Joins("LEFT JOIN provider_platforms pp ON c.provider_platform_id = pp.id").
Joins("LEFT JOIN favorites f ON f.course_id = c.id AND f.user_id = ?", userId).
Where("c.deleted_at IS NULL").
Where("pp.deleted_at IS NULL")
for i, tag := range tags {
Expand Down Expand Up @@ -53,7 +51,6 @@ type UserCourses struct {
ProviderName string `json:"provider_platform_name"`
ExternalURL string `json:"external_url"`
CourseProgress float64 `json:"course_progress"`
IsFavorited bool `json:"is_favorited"`
TotalTime uint `json:"total_time"`
}

Expand All @@ -70,7 +67,6 @@ func (db *DB) GetUserCourses(userId uint, order string, orderBy string, search s
"course_name": "c.name",
"provider_name": "pp.name",
"course_progress": "course_progress",
"is_favorited": "is_favorited",
"total_time": "a.total_time",
}
dbField, ok := fieldMap[orderBy]
Expand All @@ -82,7 +78,6 @@ func (db *DB) GetUserCourses(userId uint, order string, orderBy string, search s
tx := db.Table("courses c").
Select(`c.id, c.thumbnail_url,
c.name as course_name, pp.name as provider_name, c.external_url,
f.user_id IS NOT NULL as is_favorited,
CASE
WHEN EXISTS (SELECT 1 FROM outcomes o WHERE o.course_id = c.id AND o.user_id = ?) THEN 100
WHEN c.total_progress_milestones = 0 THEN 0
Expand All @@ -100,7 +95,6 @@ func (db *DB) GetUserCourses(userId uint, order string, orderBy string, search s
) a on a.course_id = c.id
and a.user_id = m.user_id
and a.RN = 1`).
Joins("LEFT JOIN favorites f ON f.course_id = c.id AND f.user_id = m.user_id").
Joins("LEFT JOIN outcomes o ON o.course_id = c.id AND o.user_id = m.user_id").
Where("c.deleted_at IS NULL").
Where("pp.deleted_at IS NULL")
Expand All @@ -113,8 +107,6 @@ func (db *DB) GetUserCourses(userId uint, order string, orderBy string, search s
for i, tag := range tags {
var query string
switch tag {
case "is_favorited":
query = "f.user_id IS NOT NULL"
case "completed":
query = "o.type IS NOT NULL"
case "in_progress":
Expand All @@ -127,7 +119,7 @@ func (db *DB) GetUserCourses(userId uint, order string, orderBy string, search s
}
}

tx.Group("c.id, c.name, c.thumbnail_url, pp.name, c.external_url, f.user_id, a.total_time")
tx.Group("c.id, c.name, c.thumbnail_url, pp.name, c.external_url, a.total_time")
err := tx.Scan(&courses).Error
if err != nil {
return nil, 0, 0, NewDBError(err, "error getting user programs")
Expand Down
10 changes: 5 additions & 5 deletions backend/src/database/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,16 @@ func (db *DB) UpdateUser(user *models.User) error {
return nil
}

func (db *DB) ToggleUserFavorite(user_id uint, id uint) (bool, error) {
func (db *DB) ToggleProgramFavorite(user_id uint, id uint) (bool, error) {
var favRemoved bool
var favorite models.UserFavorite
if db.First(&favorite, "user_id = ? AND course_id = ?", user_id, id).Error == nil {
if err := db.Delete(&favorite).Error; err != nil {
var favorite models.ProgramFavorite
if db.First(&favorite, "user_id = ? AND program_id = ?", user_id, id).Error == nil {
if err := db.Unscoped().Delete(&favorite).Error; err != nil {
return favRemoved, newDeleteDBError(err, "favorites")
}
favRemoved = true
} else {
favorite = models.UserFavorite{UserID: user_id, CourseID: id}
favorite = models.ProgramFavorite{UserID: user_id, ProgramID: id}
if err := db.Create(&favorite).Error; err != nil {
return favRemoved, newCreateDBError(err, "error creating favorites")
}
Expand Down
21 changes: 0 additions & 21 deletions backend/src/handlers/courses_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ func (srv *Server) registerCoursesRoutes() []routeDef {
return []routeDef{
{"GET /api/courses", srv.handleIndexCourses, true, axx},
{"GET /api/courses/{id}", srv.handleShowCourse, false, axx},
{"PUT /api/courses/{id}/save", srv.handleFavoriteCourse, true, axx},
}
}

Expand Down Expand Up @@ -51,23 +50,3 @@ func (srv *Server) handleShowCourse(w http.ResponseWriter, r *http.Request, log
}
return writeJsonResponse(w, http.StatusOK, course)
}

func (srv *Server) handleFavoriteCourse(w http.ResponseWriter, r *http.Request, log sLog) error {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
return newInvalidIdServiceError(err, "course ID")
}
user_id := srv.userIdFromRequest(r)
favoriteRemoved, err := srv.Db.ToggleUserFavorite(user_id, uint(id))
if err != nil {
log.add("course_id", id)
log.add("user_id", user_id)
return newDatabaseServiceError(err)
}
log.debugf("Favorite removed: %v", favoriteRemoved)
if favoriteRemoved {
w.WriteHeader(http.StatusNoContent)
return nil
}
return writeJsonResponse(w, http.StatusOK, "Favorite updated successfully")
}
36 changes: 0 additions & 36 deletions backend/src/handlers/courses_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"slices"
"strconv"
"testing"
Expand Down Expand Up @@ -81,41 +80,6 @@ func TestHandleShowCourse(t *testing.T) {
}
}

func TestHandleFavoriteCourse(t *testing.T) {
httpTests := []httpTest{
{"TestUserCanToggleFavoriteCourseOnOff", "student", map[string]any{"id": "4", "message": "Favorite updated successfully"}, http.StatusOK, ""},
{"TestAdminCanToggleFavoriteCourseOnOff", "admin", map[string]any{"id": "4", "message": "Favorite updated successfully"}, http.StatusOK, ""},
}
for _, test := range httpTests {
t.Run(test.testName, func(t *testing.T) {
req, err := http.NewRequest(http.MethodPut, "/api/courses/{id}/save", nil)
if err != nil {
t.Fatalf("unable to create new request, error is %v", err)
}
req.SetPathValue("id", test.mapKeyValues["id"].(string))
handler := getHandlerByRole(server.handleFavoriteCourse, test.role)
rr := executeRequest(t, req, handler, test)
if test.expectedStatusCode == http.StatusOK {
received := rr.Body.String()
data := models.Resource[struct{}]{}
if err := json.Unmarshal([]byte(received), &data); err != nil {
t.Errorf("failed to unmarshal response error is %v", err)
}
if data.Message != test.mapKeyValues["message"] {
t.Errorf("handler returned wrong body: got %v want %v", data.Message, test.mapKeyValues["message"])
}
//special case here, execute another request to toggle the user's favorite
req.SetPathValue("id", test.mapKeyValues["id"].(string))
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusNoContent {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNoContent)
}
}
})
}
}

func getCourseSearch(search string) map[string]any {
form := make(map[string]any)
_, courses, err := server.Db.GetCourse(1, 10, search)
Expand Down
1 change: 0 additions & 1 deletion backend/src/handlers/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ func TestHandleUserCourses(t *testing.T) {
{"TestGetUserCoursesWithTagsAndOrderByProvNmDescAsUser", "student", getUserCoursesSearch(4, "asc", "provider_name", "", []string{"certificate", "grade", "progress_completion"}), http.StatusOK, "?tags=certificate,grade,progress_completion,pathway_completion,college_credit&order=asc&order_by=provider_name"},
{"TestGetUserCoursesWithTagsAndOrderByCoursePgrDescAsUser", "student", getUserCoursesSearch(4, "desc", "course_progress", "", []string{"certificate", "grade", "progress_completion"}), http.StatusOK, "?tags=certificate,grade,progress_completion,pathway_completion,college_credit&order=desc&order_by=course_progress"},
{"TestGetUserCoursesWithTagsAndOrderByProvNmDescAsUser", "student", getUserCoursesSearch(4, "asc", "provider_name", "", []string{"certificate", "grade", "progress_completion"}), http.StatusOK, "?tags=certificate,grade,progress_completion,pathway_completion,college_credit&order=asc&order_by=provider_name"},
{"TestGetUserCoursesWithTagsAndOrderByIsFavdDescAsUser", "student", getUserCoursesSearch(4, "desc", "is_favorited", "", []string{"certificate", "grade", "progress_completion"}), http.StatusOK, "?tags=certificate,grade,progress_completion,pathway_completion,college_credit&order=desc&order_by=is_favorited"},
{"TestGetUserCoursesWithTagsAndOrderByTotalTmDescAsUser", "student", getUserCoursesSearch(4, "desc", "total_time", "", []string{"certificate", "grade", "progress_completion"}), http.StatusOK, "?tags=certificate,grade,progress_completion,pathway_completion,college_credit&order=desc&order_by=total_time"},
{"TestUserCoursesWithSearchAscAsUser", "student", getUserCoursesSearch(4, "asc", "", "of", []string{"certificate", "grade", "progress_completion", "pathway_completion", "college_credit"}), http.StatusOK, "?tags=certificate,grade,progress_completion,pathway_completion,college_credit&order=asc&search=of"},
{"TestUserCoursesWithSearchDescAsUser", "student", getUserCoursesSearch(4, "desc", "", "Intro", []string{"certificate", "grade", "progress_completion", "pathway_completion", "college_credit"}), http.StatusOK, "?tags=certificate,grade,progress_completion,pathway_completion,college_credit&order=desc&search=Intro"},
Expand Down
2 changes: 1 addition & 1 deletion backend/src/handlers/milestones_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func getNewMilestoneModelAndForm() (models.Milestone, map[string]any, error) {

func getNewMilestoneForm() map[string]any {
form := make(map[string]any)
_, courses, err := server.Db.GetProgram(1, 10, nil, "")
_, courses, err := server.Db.GetCourse(1, 10, "")
if err != nil {
form["err"] = err
}
Expand Down
5 changes: 3 additions & 2 deletions backend/src/handlers/programs_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ func (srv *Server) registerProgramsRoutes() []routeDef {
*/
func (srv *Server) handleIndexPrograms(w http.ResponseWriter, r *http.Request, log sLog) error {
page, perPage := srv.getPaginationInfo(r)
userId := srv.userIdFromRequest(r)
search := r.URL.Query().Get("search")
tags := r.URL.Query()["tags"]
var tagsSplit []string
if len(tags) > 0 {
tagsSplit = strings.Split(tags[0], ",")
}
total, programs, err := srv.Db.GetProgram(page, perPage, tagsSplit, search)
total, programs, err := srv.Db.GetProgram(page, perPage, tagsSplit, search, userId)
if err != nil {
log.add("search", search)
return newDatabaseServiceError(err)
Expand Down Expand Up @@ -150,7 +151,7 @@ func (srv *Server) handleFavoriteProgram(w http.ResponseWriter, r *http.Request,
return newInvalidIdServiceError(err, "program ID")
}
user_id := srv.userIdFromRequest(r)
favoriteRemoved, err := srv.Db.ToggleUserFavorite(user_id, uint(id))
favoriteRemoved, err := srv.Db.ToggleProgramFavorite(user_id, uint(id))
if err != nil {
log.add("programId", id)
log.add("userId", user_id)
Expand Down
4 changes: 2 additions & 2 deletions backend/src/handlers/programs_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func TestHandleFavoriteProgram(t *testing.T) {
}

func getAllPrograms() map[string]any {
total, programs, err := server.Db.GetProgram(1, 10, nil, "")
total, programs, err := server.Db.GetProgram(1, 10, nil, "", 1)
form := make(map[string]any)
form["programs"] = programs
form["err"] = err
Expand All @@ -263,7 +263,7 @@ func getAllPrograms() map[string]any {
}

func getProgramsBySearch(tags string, search string) map[string]any {
total, programs, err := server.Db.GetProgram(1, 10, strings.Split(tags, ","), search)
total, programs, err := server.Db.GetProgram(1, 10, strings.Split(tags, ","), search, 1)
form := make(map[string]any)
form["programs"] = programs
form["err"] = err
Expand Down
6 changes: 3 additions & 3 deletions backend/src/handlers/sections_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func TestHandleDeleteSection(t *testing.T) {

func getProgramSection(facilityId uint) map[string]any {
form := make(map[string]any)
_, programs, err := server.Db.GetProgram(1, 10, nil, "")
_, programs, err := server.Db.GetProgram(1, 10, nil, "", 1)
if err != nil {
form["err"] = err
}
Expand All @@ -286,7 +286,7 @@ func getProgramSection(facilityId uint) map[string]any {
}
func getProgramId() map[string]any {
form := make(map[string]any)
_, programs, err := server.Db.GetProgram(1, 10, nil, "")
_, programs, err := server.Db.GetProgram(1, 10, nil, "", 1)
if err != nil {
form["err"] = err
}
Expand All @@ -296,7 +296,7 @@ func getProgramId() map[string]any {

func getSectionId() map[string]any {
form := make(map[string]any)
_, programs, err := server.Db.GetProgram(1, 10, nil, "")
_, programs, err := server.Db.GetProgram(1, 10, nil, "", 1)
if err != nil {
form["err"] = err
}
Expand Down
15 changes: 9 additions & 6 deletions backend/src/models/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ func (LeftMenuLink) TableName() string {
return "left_menu_links"
}

type UserFavorite struct {
ID uint `gorm:"primaryKey" json:"-"`
UserID uint `json:"user_id"`
CourseID uint `json:"course_id"`
type ProgramFavorite struct {
ID uint `gorm:"primaryKey" json:"-"`
UserID uint `json:"user_id"`
ProgramID uint `json:"program_id"`

User *User `json:"user,omitempty" gorm:"foreignKey:UserID;constraint:OnDelete CASCADE"`
Program *Program `json:"program,omitempty" gorm:"foreignKey:ProgramID;constraint:OnDelete CASCADE"`
}

func (UserFavorite) TableName() string {
return "favorites"
func (ProgramFavorite) TableName() string {
return "program_favorites"
}
6 changes: 4 additions & 2 deletions backend/src/models/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ type Program struct {
CreditType string `json:"credit_type" gorm:"not null" validate:"required,max=50"`
ProgramStatus string `json:"program_status" gorm:"not null" validate:"required,max=50"`
ProgramType string `json:"program_type" gorm:"not null" validate:"required,max=50"`
IsFavorited bool `json:"is_favorited" gorm:"-"`

Tags []ProgramTag `json:"tags" gorm:"foreignKey:ProgramID;references:ID"`
Facilities []Facility `json:"facilities" gorm:"many2many:facilities_programs;"`
Tags []ProgramTag `json:"tags" gorm:"foreignKey:ProgramID;references:ID"`
Facilities []Facility `json:"facilities" gorm:"many2many:facilities_programs;"`
Favorites []ProgramFavorite `json:"-" gorm:"foreignKey:ProgramID;references:ID"`
}

func (Program) TableName() string { return "programs" }
Expand Down
10 changes: 5 additions & 5 deletions backend/src/models/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ type User struct {
FacilityID uint `json:"facility_id"`

/* foreign keys */
Mappings []ProviderUserMapping `json:"mappings,omitempty" gorm:"foreignKey:UserID;constraint:OnDelete CASCADE"`
FavoriteVideos []VideoFavorite `json:"favorite_videos,omitempty" goem:"foreignKey:UserID;constraint:OnDelete CASCADE"`
FavoriteCourses []UserFavorite `json:"favorite_courses,omitempty" gorm:"foreignKey:UserID;constraint:OnDelete CASCADE"`
Facility *Facility `json:"facility,omitempty" gorm:"foreignKey:FacilityID;constraint:OnDelete SET NULL"`
UserRole *Role `json:"-" gorm:"foreignKey:Role;constraint:OnDelete SET NULL"`
Mappings []ProviderUserMapping `json:"mappings,omitempty" gorm:"foreignKey:UserID;constraint:OnDelete CASCADE"`
FavoriteVideos []VideoFavorite `json:"favorite_videos,omitempty" goem:"foreignKey:UserID;constraint:OnDelete CASCADE"`
FavoritePrograms []ProgramFavorite `json:"favorite_programs,omitempty" gorm:"foreignKey:UserID;constraint:OnDelete CASCADE"`
Facility *Facility `json:"facility,omitempty" gorm:"foreignKey:FacilityID;constraint:OnDelete SET NULL"`
UserRole *Role `json:"-" gorm:"foreignKey:Role;constraint:OnDelete SET NULL"`
}

type ImportUser struct {
Expand Down
Loading

0 comments on commit eee02b1

Please sign in to comment.