Skip to content

Commit 57f61e0

Browse files
authored
Merge pull request #17 from zachspang/dashboard
[Feature] Upload and display content
2 parents a591c83 + 9ae0b0b commit 57f61e0

File tree

10 files changed

+514
-47
lines changed

10 files changed

+514
-47
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
.env
1+
.env
2+
content

backend/cmd/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
// initServer initializes the server and returns the Fiber server and the initialized database connection.
1717
func initServer() (*fiber.App, *gorm.DB, error) {
18-
app := fiber.New(fiber.Config{})
18+
app := fiber.New(fiber.Config{BodyLimit: 500 * 1024 * 1024})
1919

2020
// Initialize the database configuration
2121
conf := entity.Config{

backend/internal/entity/content.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ type Content struct {
66
gorm.Model
77
Title string `gorm:"size:255;not null;" json:"title"`
88
Path string `gorm:"size:255;not null;" json:"path"`
9+
Body string `gorm:"size:2000;" json:"body"`
10+
Type string `gorm:"size:255;not null;" json:"type"`
911
ModuleID uint
1012
Module Module `gorm:"foreignKey:ModuleID;references:ID" json:"-"`
1113
}

backend/internal/entity/course.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type Course struct {
77
Title string `gorm:"size:255;not null;" json:"title"`
88
Description string `gorm:"size:255;not null;" json:"description"`
99
CreatorID int `gorm:"foreignKey" json:"creator_id"`
10-
Students []Account `gorm:"many2many:enrollment;" json:"students"`
10+
Students []Account `gorm:"many2many:enrollment;" json:"-"`
1111
Tags []Tag `gorm:"many2many:course_tag;" json:"tags"`
1212
Modules []Module `json:"modules"`
1313
}

backend/internal/handlers/courses.go

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package handlers
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
7+
"strings"
68

79
"github.com/gofiber/fiber/v2"
810
"github.com/sandbox-science/online-learning-platform/configs/database"
@@ -83,8 +85,8 @@ func Modules(c *fiber.Ctx) error {
8385
})
8486
}
8587

86-
// Content function retrieves the content of a module
87-
func Content(c *fiber.Ctx) error {
88+
// AllContent function retrieves the content of a module
89+
func AllContent(c *fiber.Ctx) error {
8890
module_id := c.Params("module_id")
8991

9092
var module entity.Module
@@ -108,6 +110,22 @@ func Content(c *fiber.Ctx) error {
108110
})
109111
}
110112

113+
// Content function retrieves a single peice of content by id
114+
func Content(c *fiber.Ctx) error {
115+
content_id := c.Params("content_id")
116+
117+
var content entity.Content
118+
// Check if the module exists
119+
if err := database.DB.Where("id = ?", content_id).First(&content).Error; err != nil {
120+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Content not found"})
121+
}
122+
123+
return c.JSON(fiber.Map{
124+
"message": "Content successfully retrieved",
125+
"content": content,
126+
})
127+
}
128+
111129
// CreateCourse creates a course and adds it to the database.
112130
func CreateCourse(c *fiber.Ctx) error {
113131
creator_id, err := strconv.Atoi(c.Params("creator_id"))
@@ -200,7 +218,7 @@ func CreateModule(c *fiber.Ctx) error {
200218

201219
//Verify that the account attempting to create a module is the creator of the course
202220
if course.CreatorID != creator_id {
203-
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User is not the course creator"})
221+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
204222
}
205223

206224
// Create module
@@ -232,8 +250,8 @@ func CreateContent(c *fiber.Ctx) error {
232250
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid module_id"})
233251
}
234252

235-
var data map[string]string
236-
if err := c.BodyParser(&data); err != nil {
253+
title := c.FormValue("title")
254+
if title == "" {
237255
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid input"})
238256
}
239257

@@ -245,18 +263,16 @@ func CreateContent(c *fiber.Ctx) error {
245263

246264
//Verify that the account attempting to create content is the creator of the course
247265
if module.Course.CreatorID != creator_id {
248-
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User is not the course creator"})
266+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
249267
}
250-
251-
// Create content
268+
269+
// Create content database entry
252270
content := entity.Content{
253-
Title: data["title"],
271+
Title: title,
254272
Module: module,
255273
}
256274

257-
content.Path = fmt.Sprintf("content/%d/%d", module.Course.ID, content.ID)
258-
259-
// Add content to database
275+
// Create content entry
260276
if err := database.DB.Create(&content).Error; err != nil {
261277
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
262278
}
@@ -266,3 +282,126 @@ func CreateContent(c *fiber.Ctx) error {
266282
"content": content,
267283
})
268284
}
285+
286+
// Edit content inside a module
287+
func EditContent(c *fiber.Ctx) error {
288+
creator_id, err := strconv.Atoi(c.Params("creator_id"))
289+
if err != nil {
290+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid creator_id"})
291+
}
292+
293+
content_id, err := strconv.Atoi(c.Params("content_id"))
294+
if err != nil {
295+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid content_id"})
296+
}
297+
298+
var content entity.Content
299+
// Check if the content exists
300+
if err := database.DB.Model(entity.Content{}).Preload("Module.Course").Where("id = ?", content_id).First(&content).Error; err != nil {
301+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
302+
}
303+
304+
module := content.Module
305+
//Verify that the account attempting to edit is the creator of the course
306+
if module.Course.CreatorID != creator_id {
307+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
308+
}
309+
310+
title := c.FormValue("title")
311+
if title != "" {
312+
if err := database.DB.Model(&content).Update("title", title).Error; err != nil{
313+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
314+
}
315+
}
316+
317+
body := c.FormValue("body")
318+
if body != "" {
319+
if err := database.DB.Model(&content).Update("body", body).Error; err != nil{
320+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
321+
}
322+
}
323+
324+
file, err := c.FormFile("file");
325+
if err != nil {
326+
if err.Error() != "there is no uploaded file associated with the given key"{
327+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid file"})
328+
}
329+
}
330+
331+
if file != nil {
332+
var fileExtension = file.Filename[strings.LastIndex(file.Filename, "."):]
333+
334+
if err := os.MkdirAll(fmt.Sprintf("./content/%d/", module.Course.ID), 0777); err != nil{
335+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
336+
}
337+
338+
path := fmt.Sprintf("/%d/%s", module.Course.ID, strconv.FormatUint(uint64(content.ID), 10) + fileExtension)
339+
340+
//Remove previous attachment if there is one
341+
if _, err := os.Stat("./content"+path); os.IsExist(err){
342+
if err := os.Remove("./content"+path); err != nil{
343+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
344+
}
345+
}
346+
347+
if err := c.SaveFile(file, "./content"+path); err != nil{
348+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
349+
}
350+
351+
// Add the path
352+
if err := database.DB.Model(&content).Update("path", path).Error; err != nil{
353+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
354+
}
355+
356+
// Add the type
357+
filetype := file.Header.Get("Content-Type")
358+
if err := database.DB.Model(&content).Update("type", filetype).Error; err != nil{
359+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
360+
}
361+
}
362+
363+
return c.JSON(fiber.Map{
364+
"message": "Successfully updated content",
365+
})
366+
}
367+
368+
// Delete file from content
369+
func DeleteFile(c *fiber.Ctx) error {
370+
creator_id, err := strconv.Atoi(c.Params("creator_id"))
371+
if err != nil {
372+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid creator_id"})
373+
}
374+
375+
content_id, err := strconv.Atoi(c.Params("content_id"))
376+
if err != nil {
377+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid content_id"})
378+
}
379+
380+
var content entity.Content
381+
// Check if the content exists
382+
if err := database.DB.Model(entity.Content{}).Preload("Module.Course").Where("id = ?", content_id).First(&content).Error; err != nil {
383+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
384+
}
385+
386+
module := content.Module
387+
//Verify that the account attempting to edit is the creator of the course
388+
if module.Course.CreatorID != creator_id {
389+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
390+
}
391+
392+
//Delete file
393+
if _, err := os.Stat("./content"+content.Path); os.IsExist(err){
394+
if err := os.Remove("./content"+content.Path); err != nil{
395+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
396+
}
397+
}
398+
399+
//Update database entry
400+
if err := database.DB.Model(&content).Update("path", "").Error; err != nil{
401+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
402+
}
403+
404+
return c.JSON(fiber.Map{
405+
"message": "Successfully deleted file",
406+
})
407+
}

backend/internal/router/route.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,12 @@ func SetupRoutes(app *fiber.App) {
8282
app.Get("/courses/:user_id", handlers.Courses)
8383
app.Get("/course/:course_id", handlers.Course)
8484
app.Get("/modules/:course_id", handlers.Modules)
85-
app.Get("/content/:module_id", handlers.Content)
85+
app.Get("/content/:content_id", handlers.Content)
86+
app.Get("/all-content/:module_id", handlers.AllContent)
8687
app.Post("/create-course/:creator_id", handlers.CreateCourse)
8788
app.Post("/create-module/:creator_id/:course_id", handlers.CreateModule)
8889
app.Post("/create-content/:creator_id/:module_id", handlers.CreateContent)
90+
app.Post("/delete-file/:creator_id/:content_id", handlers.DeleteFile)
91+
app.Post("/edit-content/:creator_id/:content_id", handlers.EditContent)
8992
app.Post("/enroll/:user_id/:course_id", handlers.Enroll)
9093
}

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ services:
2727
- ./backend/.env
2828
volumes:
2929
- ./backend:/app
30+
- ./content:/app/content
3031

3132
backend-test:
3233
build:
@@ -54,6 +55,7 @@ services:
5455
- WATCHPACK_POLLING=true
5556
volumes:
5657
- './frontend:/app'
58+
- ./content:/app/public/content
5759

5860
volumes:
5961
learning-platform-data:

frontend/src/components/Modal.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from 'react';
22

3-
export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandler }) => {
3+
export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandler, fileUploadHandler, deleteFileHandler}) => {
44
const [showModal, setShowModal] = useState(false);
55

66
const close = () => {
@@ -14,12 +14,20 @@ export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandl
1414
const fieldList = Object.entries(inputFields).map(([name, text]) => (
1515
<div className="p-2" key={name}>
1616
<label className="block font-medium">{text + ":"}</label>
17-
<input
18-
className="border-2 border-gray-300 rounded p-2 size-11/12"
19-
type="text"
20-
name={name}
21-
onChange={changeHandler}
22-
/>
17+
{name === "body" ?
18+
<textarea
19+
className="border-2 border-gray-300 rounded p-2 size-11/12"
20+
type="text"
21+
name={name}
22+
onChange={changeHandler}
23+
/>:
24+
<input
25+
className="border-2 border-gray-300 rounded p-2 size-11/12"
26+
type="text"
27+
name={name}
28+
onChange={changeHandler}
29+
/>}
30+
2331
</div>
2432
));
2533

@@ -37,6 +45,22 @@ export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandl
3745
<div className="fixed z-[1050] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-4 rounded-lg shadow-lg border-2 border-slate-300 absolute min-w-80">
3846
<h3 className="font-semibold text-lg mb-4">{title}</h3>
3947
{fieldList}
48+
{fileUploadHandler != null ? (
49+
<div>
50+
<h1>File Upload</h1>
51+
<input type="file" onChange={fileUploadHandler}/>
52+
</div>) : null
53+
}
54+
{deleteFileHandler != null ? (
55+
<div>
56+
<button
57+
className="px-4 py-2 mt-2 border border-gray-300 rounded bg-red-500 text-white hover:bg-red-700"
58+
onClick={deleteFileHandler}
59+
>
60+
Delete Current File
61+
</button>
62+
</div>) : null
63+
}
4064
<div className="mt-4 flex justify-end space-x-2">
4165
<button
4266
className="px-4 py-2 border border-gray-300 rounded bg-gray-200 hover:bg-gray-300"

0 commit comments

Comments
 (0)