Skip to content

Commit 5e3ac1d

Browse files
authored
Merge pull request #4 from CS3219-AY2425S1/question-page
Add Question List Page
2 parents 76bf68b + e2ff470 commit 5e3ac1d

36 files changed

+1263
-153
lines changed

backend/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
questionDB.env
2+
log

backend/common/logger_struct.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package common
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
)
6+
7+
//contains the logger
8+
type Logger struct {
9+
Log *logrus.Logger
10+
}
11+
12+
func NewLogger(logger *logrus.Logger) *Logger {
13+
return &Logger{Log: logger}
14+
}

backend/common/question_struct.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// defines the JSON format of quesitons.
2+
package common
3+
4+
type Difficulty int
5+
6+
const (
7+
Easy Difficulty = 1
8+
Medium Difficulty = 2
9+
Hard Difficulty = 3
10+
)
11+
12+
// Question struct
13+
type Question struct {
14+
ID int `json:"id"`
15+
Difficulty Difficulty `json:"difficulty"`
16+
Title string `json:"title"`
17+
Description string `json:"description"`
18+
Categories []string `json:"categories"`
19+
TestCases map[string]string `json:"test_cases"`
20+
//Images []string `json:"images"` // for future uses
21+
}
22+
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// contains the database-related functions for the questions API.
2+
package database
3+
4+
import (
5+
"context"
6+
"errors"
7+
"net/http"
8+
"fmt"
9+
10+
"go.mongodb.org/mongo-driver/bson"
11+
"go.mongodb.org/mongo-driver/mongo/options"
12+
"peerprep/common"
13+
)
14+
15+
func (db *QuestionDB) GetAllQuestionsWithQuery(logger *common.Logger, filter bson.D) ([]common.Question, error) {
16+
questionCursor, err := db.questions.Find(context.Background(), filter)
17+
18+
if err != nil {
19+
logger.Log.Error("Error retrieving questions: ", err.Error())
20+
return nil, err
21+
}
22+
23+
var questions []common.Question
24+
25+
if err = questionCursor.All(context.Background(), &questions); err != nil {
26+
logger.Log.Error("Error decoding questions: ", err.Error())
27+
return nil, err
28+
}
29+
30+
return questions, nil
31+
}
32+
33+
func (db *QuestionDB) AddQuestion(logger *common.Logger, question *common.Question) (int, error) {
34+
if db.QuestionExists(question) {
35+
logger.Log.Warn("Cannot add question: question already exists")
36+
return http.StatusConflict, errors.New("question already exists")
37+
}
38+
39+
question.ID = db.FindNextQuestionId()
40+
41+
if question.ID == -1 {
42+
logger.Log.Error("Could not find next question ID")
43+
return http.StatusBadGateway, errors.New("could not find the next question ID")
44+
}
45+
46+
if _, err := db.questions.InsertOne(context.Background(), question); err != nil {
47+
logger.Log.Error("Error adding question", err.Error())
48+
return http.StatusBadGateway, err
49+
}
50+
51+
db.IncrementNextQuestionId(question.ID + 1, logger)
52+
return http.StatusOK, nil
53+
}
54+
55+
func (db *QuestionDB) UpsertQuestion(logger *common.Logger, question *common.Question) (int, error) {
56+
57+
filter := bson.D{bson.E{Key: "id", Value: question.ID}}
58+
setter := bson.M{"$set": question}
59+
upsert := options.Update().SetUpsert(true)
60+
61+
_, err := db.questions.UpdateOne(context.Background(), filter, setter, upsert)
62+
63+
if err != nil {
64+
logger.Log.Error("Error while upserting question", err.Error())
65+
return http.StatusBadGateway, err
66+
}
67+
68+
return http.StatusOK, nil
69+
}
70+
71+
func (db *QuestionDB) DeleteQuestion(logger *common.Logger, id int) (int, error) {
72+
deleteStatus, err := db.questions.DeleteOne(context.Background(), bson.D{bson.E{Key: "id", Value: id}})
73+
74+
if err != nil {
75+
logger.Log.Error("Error deleting question", err.Error())
76+
return http.StatusBadGateway, err
77+
} else if deleteStatus.DeletedCount == 0 {
78+
msg := fmt.Sprintf("Question with ID %d not found when deleting question", id)
79+
logger.Log.Warn(msg)
80+
return http.StatusNotFound, errors.New(msg)
81+
}
82+
83+
return http.StatusNoContent, nil
84+
}

backend/database/database_util.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// utility functions for questions api
2+
package database
3+
4+
import (
5+
"context"
6+
"fmt"
7+
8+
"go.mongodb.org/mongo-driver/bson"
9+
"peerprep/common"
10+
)
11+
12+
// used to check if a question already exists in the database.
13+
func (db *QuestionDB) QuestionExists(question *common.Question) bool {
14+
filter := bson.D{bson.E{Key: "title", Value: question.Title}}
15+
err := db.questions.FindOne(context.Background(), filter).Decode(&common.Question{}) //FindOne() returns error if no document is found
16+
return err == nil
17+
}
18+
19+
// used to find the next question ID to be used. Similar to SERIAL in psql.
20+
func (db *QuestionDB) FindNextQuestionId() int {
21+
id := struct {
22+
Next int `json:"next"`
23+
}{}
24+
filter := bson.D{}
25+
26+
if err := db.nextId.FindOne(context.Background(), filter).Decode(&id); err != nil {
27+
return -1
28+
}
29+
30+
return id.Next
31+
}
32+
33+
// used to increment the next question ID to be used.
34+
// since the collection is capped at one document, inserting a new document will replace the old one.
35+
func (db *QuestionDB) IncrementNextQuestionId(nextId int, logger *common.Logger) {
36+
var err error
37+
if _, err = db.nextId.InsertOne(context.Background(), bson.D{bson.E{Key: "next", Value: nextId}}); err != nil {
38+
logger.Log.Error("Error incrementing next question ID: ", err)
39+
return
40+
}
41+
42+
logger.Log.Info(fmt.Sprintf("Next question ID incremented to %d successfully", nextId))
43+
}
44+
45+
// used to check if a question being replaced will cause duplicates in the database
46+
47+
func (db *QuestionDB) QuestionExistsExceptId(question *common.Question) bool {
48+
filter := bson.D{bson.E{Key: "title", Value: question.Title}, bson.E{Key: "id", Value: bson.D{bson.E{Key: "$ne", Value: question.ID}}}}
49+
err := db.questions.FindOne(context.Background(), filter).Decode(&common.Question{}) //FindOne() returns error if no document is found
50+
return err == nil
51+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// this is used to initialise the database connection
2+
package database
3+
4+
import (
5+
"context"
6+
"log"
7+
"os"
8+
9+
"github.com/joho/godotenv"
10+
"go.mongodb.org/mongo-driver/mongo"
11+
"go.mongodb.org/mongo-driver/mongo/options"
12+
)
13+
14+
func InitialiseDB() (*mongo.Client, error) {
15+
// Load environment variables
16+
err := godotenv.Load("questionDB.env")
17+
18+
if err != nil {
19+
log.Fatal("Error loading environment variables: " + err.Error())
20+
}
21+
22+
mongoURI := os.Getenv("MONGODB_URI")
23+
24+
if mongoURI == "" {
25+
log.Fatal("MONGODB_URI not set in environment variables")
26+
}
27+
28+
clientOptions := options.Client().ApplyURI(mongoURI)
29+
30+
// Connect to MongoDB
31+
server, err := mongo.Connect(context.Background(), clientOptions)
32+
33+
if err != nil {
34+
log.Fatal("Error connecting to MongoDB" + err.Error())
35+
}
36+
37+
// Check the connection
38+
err = server.Ping(context.Background(), nil)
39+
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
return server, nil
45+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package database
2+
3+
import (
4+
"go.mongodb.org/mongo-driver/mongo"
5+
)
6+
7+
// QuestionDB is a struct that contains a pointer to a mongo client.
8+
// questions is the collection with all the questions, nextId is a single-entry collection that stores the next ID to be used.
9+
type QuestionDB struct {
10+
questions *mongo.Collection
11+
nextId *mongo.Collection
12+
}
13+
14+
// returns a pointer to an instance of the Question collection
15+
func NewQuestionDB(client *mongo.Client) *QuestionDB {
16+
17+
questionCollection := client.Database("questions").Collection("questions")
18+
idCollection := client.Database("questions").Collection("id")
19+
return &QuestionDB{questions: questionCollection, nextId: idCollection}
20+
}

backend/go.mod

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
module peerprep
2+
3+
go 1.20
4+
5+
require github.com/joho/godotenv v1.5.1 // indirect -allows to load environment variables from a .env file instead of hardcoding them in the code
6+
7+
require (
8+
github.com/bytedance/sonic v1.11.6 // indirect
9+
github.com/bytedance/sonic/loader v0.1.1 // indirect
10+
github.com/cloudwego/base64x v0.1.4 // indirect
11+
github.com/cloudwego/iasm v0.2.0 // indirect
12+
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
13+
github.com/gin-contrib/sse v0.1.0 // indirect
14+
github.com/gin-gonic/gin v1.10.0 // indirect
15+
github.com/go-playground/locales v0.14.1 // indirect
16+
github.com/go-playground/universal-translator v0.18.1 // indirect
17+
github.com/go-playground/validator/v10 v10.20.0 // indirect
18+
github.com/goccy/go-json v0.10.2 // indirect
19+
github.com/golang/snappy v0.0.4 // indirect
20+
github.com/json-iterator/go v1.1.12 // indirect
21+
github.com/klauspost/compress v1.13.6 // indirect
22+
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
23+
github.com/leodido/go-urn v1.4.0 // indirect
24+
github.com/mattn/go-isatty v0.0.20 // indirect
25+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
26+
github.com/modern-go/reflect2 v1.0.2 // indirect
27+
github.com/montanaflynn/stats v0.7.1 // indirect
28+
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
29+
github.com/sirupsen/logrus v1.9.3 // indirect - Logging library
30+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
31+
github.com/ugorji/go/codec v1.2.12 // indirect
32+
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
33+
github.com/xdg-go/scram v1.1.2 // indirect
34+
github.com/xdg-go/stringprep v1.0.4 // indirect
35+
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
36+
go.mongodb.org/mongo-driver v1.16.1 // indirect
37+
golang.org/x/arch v0.8.0 // indirect
38+
golang.org/x/crypto v0.23.0 // indirect
39+
golang.org/x/net v0.25.0 // indirect
40+
golang.org/x/sync v0.7.0 // indirect
41+
golang.org/x/sys v0.20.0 // indirect
42+
golang.org/x/text v0.15.0 // indirect
43+
google.golang.org/protobuf v1.34.1 // indirect
44+
gopkg.in/yaml.v3 v3.0.1 // indirect
45+
)
46+
47+
require github.com/gin-contrib/cors v1.7.2 // indirect

0 commit comments

Comments
 (0)