Skip to content

Commit 672eb1c

Browse files
authored
Merge pull request #14 from zachspang/dashboard
[Feature] Course dashboard
2 parents 729ae76 + 2dab89a commit 672eb1c

File tree

13 files changed

+478
-28
lines changed

13 files changed

+478
-28
lines changed

backend/configs/database/database.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ func DatabaseMigration(DB *gorm.DB) (*gorm.DB, error) {
3434
return nil, err
3535
}
3636

37+
err = DB.AutoMigrate(&entity.Course{})
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
err = DB.AutoMigrate(&entity.Tag{})
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
err = DB.AutoMigrate(&entity.Module{})
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
err = DB.AutoMigrate(&entity.Content{})
53+
if err != nil {
54+
return nil, err
55+
}
56+
3757
fmt.Println("Migrated database!")
3858

3959
return DB, nil

backend/internal/entity/account.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ type Account struct {
88
Password string `gorm:"size:255;not null;" json:"-"`
99
Email string `gorm:"size:100;not null;unique" json:"email"`
1010
Role string `gorm:"size:10;not null;check:role IN ('student', 'educator');" json:"role"`
11+
Courses []*Course `gorm:"many2many:enrollment;"`
1112
}

backend/internal/entity/content.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package entity
2+
3+
import "gorm.io/gorm"
4+
5+
type Content struct {
6+
gorm.Model
7+
Title string `gorm:"size:255;not null;" json:"title"`
8+
Path string `gorm:"size:255;not null;" json:"path"`
9+
ModuleID uint
10+
Module Module `gorm:"foreignKey:ModuleID;references:ID" json:"-"`
11+
}

backend/internal/entity/course.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package entity
2+
3+
import "gorm.io/gorm"
4+
5+
type Course struct {
6+
gorm.Model
7+
Title string `gorm:"size:255;not null;" json:"title"`
8+
Description string `gorm:"size:255;not null;" json:"description"`
9+
CreatorID int `gorm:"foreignKey" json:"creator_id"`
10+
Students []Account `gorm:"many2many:enrollment;" json:"students"`
11+
Tags []Tag `gorm:"many2many:course_tag;" json:"tags"`
12+
Modules []Module `json:"modules"`
13+
}

backend/internal/entity/module.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package entity
2+
3+
import "gorm.io/gorm"
4+
5+
type Module struct {
6+
gorm.Model
7+
Title string `gorm:"size:255;not null;" json:"title"`
8+
CourseID uint
9+
Course Course `gorm:"foreignKey:CourseID;references:ID" json:"-"`
10+
Content []Content `json:"content"`
11+
}

backend/internal/entity/tag.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package entity
2+
3+
import "gorm.io/gorm"
4+
5+
type Tag struct {
6+
gorm.Model
7+
TagName string `gorm:"size:255;not null;" json:"tag_name)"`
8+
Courses []Course `gorm:"many2many:course_tag;"`
9+
}
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/gofiber/fiber/v2"
8+
"github.com/sandbox-science/online-learning-platform/configs/database"
9+
"github.com/sandbox-science/online-learning-platform/internal/entity"
10+
)
11+
12+
// Courses function retrieves enrolled course titles and descriptions based on user_id from the URL
13+
func Courses(c *fiber.Ctx) error {
14+
15+
user_id := c.Params("user_id")
16+
17+
var user entity.Account
18+
// Check if the user exists
19+
if err := database.DB.Where("id = ?", user_id).First(&user).Error; err != nil {
20+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
21+
}
22+
23+
var courses []entity.Course
24+
// Get all courses that user is enrolled in
25+
if err := database.DB.Model(&entity.Course{}).Where("id IN (?)", database.DB.Table("enrollment").Select("course_id").Where("account_id = ?", user_id)).Find(&courses).Error; err != nil {
26+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Error retrieving courses"})
27+
}
28+
29+
if len(courses) == 0{
30+
return c.JSON(fiber.Map{
31+
"message": "Not enrolled in any courses",
32+
"courses": courses,
33+
})
34+
}
35+
36+
return c.JSON(fiber.Map{
37+
"message": "Courses successfully retrieved",
38+
"courses": courses,
39+
})
40+
41+
}
42+
43+
// Course function retrieves an indiviudal course from its id
44+
func Course(c *fiber.Ctx) error {
45+
46+
course_id := c.Params("course_id")
47+
48+
var course entity.Course
49+
// Check if the course exists
50+
if err := database.DB.Preload("Students").Preload("Modules").Preload("Modules.Content").Preload("Tags").Where("id = ?", course_id).First(&course).Error; err != nil {
51+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
52+
}
53+
54+
return c.JSON(fiber.Map{
55+
"message": "Course successfully retrieved",
56+
"course": course,
57+
})
58+
59+
}
60+
61+
// Modules function retrieves the modules of a course
62+
func Modules(c *fiber.Ctx) error {
63+
64+
course_id := c.Params("course_id")
65+
66+
var modules []entity.Module
67+
// Check if the course exists
68+
if err := database.DB.Preload("Content").Where("course_id = ?", course_id).Find(&modules).Error; err != nil {
69+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
70+
}
71+
72+
if len(modules) == 0{
73+
return c.JSON(fiber.Map{
74+
"message": "No modules in course",
75+
"modules": modules,
76+
})
77+
}
78+
79+
return c.JSON(fiber.Map{
80+
"message": "Modules successfully retrieved",
81+
"modules": modules,
82+
})
83+
84+
}
85+
86+
// Content function retrieves the content of a module
87+
func Content(c *fiber.Ctx) error {
88+
89+
module_id := c.Params("module_id")
90+
91+
var module entity.Module
92+
// Check if the module exists
93+
if err := database.DB.Preload("Content").Where("id = ?", module_id).First(&module).Error; err != nil {
94+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Module not found"})
95+
}
96+
97+
var content = module.Content
98+
99+
if len(content) == 0{
100+
return c.JSON(fiber.Map{
101+
"message": "No content in module",
102+
"content": [] string{},
103+
})
104+
}
105+
106+
return c.JSON(fiber.Map{
107+
"message": "Content successfully retrieved",
108+
"content": content,
109+
})
110+
111+
}
112+
113+
// CreateCourse creates a course and adds it to the database.
114+
func CreateCourse(c *fiber.Ctx) error {
115+
116+
creator_id,err := strconv.Atoi(c.Params("creator_id"))
117+
if err != nil{
118+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid creator_id"})
119+
}
120+
121+
var data map[string]string;
122+
if err := c.BodyParser(&data); err != nil {
123+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid input"})
124+
}
125+
126+
//Verify that the account attempting to create a course is an educator
127+
var role string;
128+
if err := database.DB.Model(&entity.Account{}).Select("role").Where("id = ?", creator_id).First(&role).Error; err != nil {
129+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
130+
}
131+
if role != "educator"{
132+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not an educator"})
133+
}
134+
135+
// Create course
136+
course := entity.Course{
137+
Title: data["title"],
138+
Description: data["description"],
139+
CreatorID: creator_id,
140+
}
141+
142+
// Add course to database
143+
if err := database.DB.Create(&course).Error; err != nil {
144+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
145+
}
146+
147+
return c.JSON(fiber.Map{
148+
"message": "Course created successfully",
149+
"course": course,
150+
})
151+
}
152+
153+
// Enroll user into course
154+
func Enroll(c *fiber.Ctx) error {
155+
156+
user_id := c.Params("user_id")
157+
course_id := c.Params("course_id")
158+
159+
var user entity.Account
160+
// Check if the user exists
161+
if err := database.DB.Where("id = ?", user_id).First(&user).Error; err != nil {
162+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
163+
}
164+
165+
var course entity.Course
166+
// Check if the course exists
167+
if err := database.DB.Where("id = ?", course_id).First(&course).Error; err != nil {
168+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
169+
}
170+
171+
// Enroll user into course
172+
if err := database.DB.Model(&user).Association("Courses").Append(&course); err != nil {
173+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Error enrolling into course"})
174+
}
175+
176+
return c.JSON(fiber.Map{
177+
"message": fmt.Sprintf("Successfully enrolled user id %d in course %s", user.ID, course.Title),
178+
})
179+
180+
}
181+
182+
// Create a module inside a course
183+
func CreateModule(c *fiber.Ctx) error {
184+
185+
creator_id,err := strconv.Atoi(c.Params("creator_id"))
186+
if err != nil{
187+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid creator_id"})
188+
}
189+
190+
course_id,err := strconv.Atoi(c.Params("course_id"))
191+
if err != nil{
192+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid course_id"})
193+
}
194+
195+
var data map[string]string;
196+
if err := c.BodyParser(&data); err != nil {
197+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid input"})
198+
}
199+
200+
var course entity.Course
201+
// Check if the course exists
202+
if err := database.DB.Where("id = ?", course_id).First(&course).Error; err != nil {
203+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
204+
}
205+
//Verify that the account attempting to create a module is the creator of the course
206+
if course.CreatorID != creator_id{
207+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User is not the course creator"})
208+
}
209+
210+
// Create module
211+
module := entity.Module{
212+
Title: data["title"],
213+
Course: course,
214+
}
215+
216+
// Add module to database
217+
if err := database.DB.Create(&module).Error; err != nil {
218+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
219+
}
220+
221+
return c.JSON(fiber.Map{
222+
"message": "Module created successfully",
223+
"module": module,
224+
})
225+
}
226+
227+
// Create content inside a module
228+
func CreateContent(c *fiber.Ctx) error {
229+
230+
creator_id,err := strconv.Atoi(c.Params("creator_id"))
231+
if err != nil{
232+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid creator_id"})
233+
}
234+
235+
module_id,err := strconv.Atoi(c.Params("module_id"))
236+
if err != nil{
237+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid module_id"})
238+
}
239+
240+
var data map[string]string;
241+
if err := c.BodyParser(&data); err != nil {
242+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid input"})
243+
}
244+
245+
var module entity.Module
246+
// Check if the module exists
247+
if err := database.DB.Preload("Course").Where("id = ?", module_id).First(&module).Error; err != nil {
248+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Module not found"})
249+
}
250+
//Verify that the account attempting to create content is the creator of the course
251+
if module.Course.CreatorID != creator_id{
252+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User is not the course creator"})
253+
}
254+
255+
// Create content
256+
content := entity.Content{
257+
Title: data["title"],
258+
Module: module,
259+
}
260+
content.Path = fmt.Sprintf("content/%d/%d", module.Course.ID, content.ID)
261+
262+
// Add content to database
263+
if err := database.DB.Create(&content).Error; err != nil {
264+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
265+
}
266+
267+
return c.JSON(fiber.Map{
268+
"message": "Content created successfully",
269+
"content": content,
270+
})
271+
}

backend/internal/router/route.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,14 @@ func SetupRoutes(app *fiber.App) {
7777
app.Put("/update-username", handlers.UpdateUsername) // Update username
7878
app.Put("/update-email", handlers.UpdateEmail) // Update email with confirmation
7979
app.Put("/update-password", handlers.UpdatePassword) // Update password and log user off after
80+
81+
// Routes for courses
82+
app.Get("/courses/:user_id", handlers.Courses)
83+
app.Get("/course/:course_id", handlers.Course)
84+
app.Get("/modules/:course_id", handlers.Modules)
85+
app.Get("/content/:module_id", handlers.Content)
86+
app.Post("/create-course/:creator_id", handlers.CreateCourse)
87+
app.Post("/create-module/:creator_id/:course_id", handlers.CreateModule)
88+
app.Post("/create-content/:creator_id/:module_id", handlers.CreateContent)
89+
app.Post("/enroll/:user_id/:course_id", handlers.Enroll)
8090
}

frontend/src/App.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import Navbar from './components/Navbar';
77
import { Home } from './components/pages/Home'
88
import { NotFound } from './components/pages/NotFound'
99
import { HealthCheck } from './components/pages/HealthCheck';
10-
import { Courses } from './components/pages/Courses'
10+
import { CourseDashboard } from './components/pages/CourseDashboard'
11+
import { Course } from './components/pages/Course'
1112
import { Login } from './components/pages/Login'
1213
import { Signup } from './components/pages/Signup'
1314
import { Account } from './components/pages/Account'
15+
import { Content } from './components/pages/Content'
1416

1517
import ProtectedRoute from "./config/ProtectedRoutes"
1618

@@ -23,7 +25,9 @@ export default function App() {
2325
<Route path='/' element={<Home />} />
2426
<Route path='*' element={<NotFound />} />
2527
<Route path='/health' element={<HealthCheck />} />
26-
<Route path='/courses' element={<ProtectedRoute><Courses /></ProtectedRoute>} />
28+
<Route path='/courses' element={<ProtectedRoute><CourseDashboard /></ProtectedRoute>} />
29+
<Route path="/courses/:courseID" element={<ProtectedRoute><Course /></ProtectedRoute>} />
30+
<Route path="/courses/:courseID/:contentID" element={<ProtectedRoute><Content /></ProtectedRoute>} />
2731
<Route path='/login' element={<Login />} />
2832
<Route path='/signup' element={<Signup />} />
2933
<Route path='/account' element={<ProtectedRoute><Account /></ProtectedRoute>} />

0 commit comments

Comments
 (0)