Compare commits
57 Commits
8a3d0fa49b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
a56621d6ae
|
|||
|
3bbeed57ed
|
|||
|
97e958697f
|
|||
|
035ab9a6c7
|
|||
|
b1008a63d9
|
|||
|
dccc1949e0
|
|||
|
8626fd9a44
|
|||
|
cab68b147c
|
|||
|
0ea4e3933f
|
|||
|
608854ebb0
|
|||
|
245b7bbb2a
|
|||
|
80ccc17eef
|
|||
|
b03c50445f
|
|||
|
35ddecb5a9
|
|||
|
35e8b2eec3
|
|||
|
ecbbcb04c0
|
|||
|
ddf8fd57a0
|
|||
|
9be7abcd05
|
|||
|
27d2317e84
|
|||
|
0740f1f4ac
|
|||
|
f513c31664
|
|||
|
c7797c9401
|
|||
|
275f0f0914
|
|||
|
c1a8f04dc7
|
|||
|
1f0e8ca905
|
|||
|
389f71c2f8
|
|||
|
2b071d35c8
|
|||
|
104370fb79
|
|||
|
8670612259
|
|||
|
29e417c9d0
|
|||
|
a6ae87abf2
|
|||
|
4faecbd29c
|
|||
|
542e8c232a
|
|||
|
f0b101386b
|
|||
|
e07b5add79
|
|||
|
0db9844e77
|
|||
|
797b7235e9
|
|||
|
a2bc3567c5
|
|||
|
24f6256703
|
|||
|
1dfa9c1421
|
|||
|
15d336ca53
|
|||
|
9c6b46f68f
|
|||
|
d5d2d830cc
|
|||
|
dcb9111288
|
|||
|
a789c2eb83
|
|||
|
b71c23d706
|
|||
|
8ec489beb3
|
|||
|
0b392ab5b1
|
|||
|
84fb1e9f4b
|
|||
|
5beaf2dba4
|
|||
|
12498c2ee8
|
|||
|
20f0309c5c
|
|||
|
e4af6457b9
|
|||
| d1a35dd2c1 | |||
| 214b1c8657 | |||
|
b1f6d6b22c
|
|||
|
d05e0fb0e0
|
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
This project uses React on the frontend and Flask on the backend. The goal of this
|
||||
project was to make a Brightspace clone/remake.
|
||||
|
||||
# Running the code
|
||||
|
||||
## Docker
|
||||
|
||||
If you have docker and docker-compose installed, you can simply use the provided
|
||||
`docker-compose.yml` file.
|
||||
|
||||
```sh
|
||||
echo "REACT_APP_BACKEND_URL=http://localhost:5000" > frontend/.env
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
Once both services are up, head to [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
**NOTE**: You might have to refresh the webpage a few times. For some reason it bugs out
|
||||
sometimes
|
||||
|
||||
## Manual
|
||||
|
||||
### Backend setup
|
||||
|
||||
Run this in a new terminal
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
flask run
|
||||
```
|
||||
|
||||
|
||||
### Frontend setup
|
||||
|
||||
Run this in a new terminal
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ RUN python -m venv venv
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
RUN . venv/bin/activate
|
||||
RUN python -m pip install -r /code/requirements.txt
|
||||
COPY venv /code/venv
|
||||
RUN cp -r venv /code/venv
|
||||
|
||||
WORKDIR /code
|
||||
COPY app app
|
||||
|
||||
@@ -1,37 +1,147 @@
|
||||
from app import db
|
||||
import sqlalchemy as sa
|
||||
from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), index=True, unique=True)
|
||||
role = db.Column(db.String(32), index=True)
|
||||
email = db.Column(db.String(120), index=True, unique=True)
|
||||
password_hash = db.Column(db.String(128))
|
||||
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
token = db.Column(db.String(32), index=True, unique=True)
|
||||
enrollment = db.Table(
|
||||
"enrollment",
|
||||
sa.Column("user_id", sa.ForeignKey("user.id"), primary_key=True),
|
||||
sa.Column("course_id", sa.ForeignKey("course.id"), primary_key=True),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
username = sa.Column(sa.String(64), index=True, unique=True)
|
||||
role = sa.Column(sa.String(32), index=True)
|
||||
email = sa.Column(sa.String(120), index=True, unique=True)
|
||||
password_hash = sa.Column(sa.String(128))
|
||||
last_seen = sa.Column(sa.DateTime, default=datetime.utcnow)
|
||||
token = sa.Column(sa.String(32), index=True, unique=True)
|
||||
enrolled_courses = db.relationship(
|
||||
"Course",
|
||||
secondary=enrollment,
|
||||
backref=db.backref("students", lazy="dynamic"),
|
||||
lazy="dynamic",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User {self.username}>"
|
||||
|
||||
def set_password(self, password):
|
||||
def set_password(self, password) -> None:
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
def check_password(self, password) -> bool:
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def to_dict(self):
|
||||
def is_enrolled(self, c) -> bool:
|
||||
return self.enrolled_courses.filter(enrollment.c.course_id == c.id).count() > 0
|
||||
|
||||
def enroll(self, c) -> bool:
|
||||
if not self.is_enrolled(c):
|
||||
self.enrolled_courses.append(c)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def unenroll(self, c) -> bool:
|
||||
if self.is_enrolled(c):
|
||||
self.enrolled_courses.remove(c)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"email": self.email,
|
||||
"role": self.role,
|
||||
}
|
||||
|
||||
def from_dict(self, data, new_user=False):
|
||||
def from_dict(self, data, new_user=False) -> None:
|
||||
for field in ["role", "username", "email"]:
|
||||
if field in data:
|
||||
setattr(self, field, data[field])
|
||||
if new_user and "password" in data:
|
||||
self.set_password(data["password"])
|
||||
|
||||
|
||||
class Course(db.Model):
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String(128), index=True)
|
||||
course_code = sa.Column(sa.String(32), index=True)
|
||||
description = sa.Column(sa.Text, index=True)
|
||||
instructor = sa.Column(sa.ForeignKey(User.id), index=True)
|
||||
created_at = sa.Column(sa.DateTime)
|
||||
assignments = db.relationship("Assignment", backref="course", lazy="dynamic")
|
||||
content = db.relationship("Content", backref="course", lazy="dynamic")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Course {self.course_code}>"
|
||||
|
||||
def from_dict(self, data) -> None:
|
||||
for field in ["name", "course_code", "description", "instructor"]:
|
||||
if field in data:
|
||||
setattr(self, field, data[field])
|
||||
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {}
|
||||
for f in ["id", "name", "course_code", "description", "created_at"]:
|
||||
d[f] = getattr(self, f)
|
||||
|
||||
d["instructor"] = User.query.get(self.instructor).username
|
||||
return d
|
||||
|
||||
|
||||
class Assignment(db.Model):
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String(128), index=True)
|
||||
course_id = sa.Column(sa.ForeignKey(Course.id), index=True)
|
||||
description = sa.Column(sa.Text, index=True)
|
||||
due_date = sa.Column(sa.DateTime)
|
||||
created_at = sa.Column(sa.DateTime)
|
||||
|
||||
def from_dict(self, data) -> None:
|
||||
for field in ["name", "course_id", "description", "due_date"]:
|
||||
if field in data:
|
||||
setattr(self, field, data[field])
|
||||
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {}
|
||||
for f in ["id", "name", "course_id", "description", "due_date", "created_at"]:
|
||||
d[f] = getattr(self, f)
|
||||
d["due_date"] = self.due_date.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class Content(db.Model):
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String(128), index=True)
|
||||
body = sa.Column(sa.Text, index=True)
|
||||
course_id = sa.Column(sa.ForeignKey(Course.id), index=True)
|
||||
created_at = sa.Column(sa.DateTime)
|
||||
|
||||
def from_dict(self, data) -> None:
|
||||
for field in ["name", "body", "course_id"]:
|
||||
if field in data:
|
||||
setattr(self, field, data[field])
|
||||
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {}
|
||||
for f in ["id", "course_id", "name", "body", "created_at"]:
|
||||
d[f] = getattr(self, f)
|
||||
|
||||
return d
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from functools import wraps
|
||||
from flask_login import login_required, login_user, logout_user
|
||||
from app.bp import bp
|
||||
from flask import jsonify, request
|
||||
@@ -5,7 +6,7 @@ from app.errors import error_response
|
||||
from flask_login import current_user
|
||||
|
||||
from app import login, db
|
||||
from app.models import User
|
||||
from app.models import Content, Course, User, Assignment
|
||||
|
||||
|
||||
@login.user_loader
|
||||
@@ -13,6 +14,33 @@ def load_user(user_id):
|
||||
return User.query.get(user_id)
|
||||
|
||||
|
||||
def check_data(data, required_fields):
|
||||
for f in required_fields:
|
||||
if f not in data:
|
||||
return f
|
||||
return None
|
||||
|
||||
|
||||
def instructor_required(func):
|
||||
@wraps(func)
|
||||
def dec(*args, **kwargs):
|
||||
if not current_user.role in ["instructor", "admin"]:
|
||||
return error_response(400, "User is not instructor!")
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
def admin_required(func):
|
||||
@wraps(func)
|
||||
def dec(*args, **kwargs):
|
||||
if current_user.role != "admin":
|
||||
return error_response(400, "User is not an admin!")
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
@bp.route("/login", methods=["POST"])
|
||||
def login_route():
|
||||
data = request.get_json()
|
||||
@@ -46,10 +74,8 @@ def logout_route():
|
||||
@bp.route("/register", methods=["POST"])
|
||||
def register():
|
||||
data = request.get_json()
|
||||
|
||||
required_fields = ["role", "username", "email", "password", "password2"]
|
||||
for f in required_fields:
|
||||
if f not in data:
|
||||
if f := check_data(data, required_fields):
|
||||
return error_response(400, f"Must supply {f}")
|
||||
|
||||
if User.query.filter_by(username=data["username"]).first():
|
||||
@@ -65,3 +91,320 @@ def register():
|
||||
|
||||
resp = jsonify(u.to_dict())
|
||||
return resp
|
||||
|
||||
|
||||
@bp.route("/course", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def create_course():
|
||||
data = request.get_json()
|
||||
|
||||
required_fields = ["name", "course_code", "description", "instructor"]
|
||||
|
||||
if f := check_data(data, required_fields):
|
||||
return error_response(400, f"Must supply {f}")
|
||||
|
||||
u = User.query.get(data["instructor"])
|
||||
if not u:
|
||||
return error_response(400, f"User with id {data['instructor']} does not exist")
|
||||
|
||||
c = Course.query.filter_by(course_code=data["course_code"]).first()
|
||||
if c:
|
||||
return error_response(
|
||||
400, f"Course with course code {data['course_code']} already exists"
|
||||
)
|
||||
|
||||
if u.role != "instructor":
|
||||
return error_response(400, "User is not instructor")
|
||||
|
||||
c = Course()
|
||||
c.from_dict(data)
|
||||
u.enroll(c)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(c.to_dict())
|
||||
|
||||
|
||||
@bp.route("/course/<string:username>", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def create_course_by_username(username):
|
||||
data = request.get_json()
|
||||
|
||||
required_fields = ["name", "course_code", "description"]
|
||||
|
||||
u = User.query.filter_by(username=username).first()
|
||||
if not u:
|
||||
return error_response(400, f"User with username {username} does not exist")
|
||||
|
||||
if f := check_data(data, required_fields):
|
||||
return error_response(400, f"Must supply {f}")
|
||||
|
||||
c = Course.query.filter_by(course_code=data["course_code"]).first()
|
||||
if c:
|
||||
return error_response(
|
||||
400, f"Course with course code {data['course_code']} already exists"
|
||||
)
|
||||
|
||||
if u.role != "instructor":
|
||||
return error_response(400, "User is not instructor")
|
||||
|
||||
data["instructor"] = str(u.id)
|
||||
c = Course()
|
||||
c.from_dict(data)
|
||||
u.enroll(c)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(c.to_dict())
|
||||
|
||||
|
||||
@bp.route("/course/<int:id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_course(id):
|
||||
c = Course.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"Course with id {id} does not exist")
|
||||
|
||||
db.session.delete(c)
|
||||
db.session.commit()
|
||||
return jsonify(c.to_dict())
|
||||
|
||||
|
||||
@bp.route("/user/<int:id>/courses", methods=["GET"])
|
||||
@login_required
|
||||
def get_courses(id):
|
||||
u = User.query.get(id)
|
||||
d = {"courses": []}
|
||||
courses = Course.query.all() if u.role == "admin" else u.enrolled_courses.all()
|
||||
for c in courses:
|
||||
d["courses"].append(c.to_dict())
|
||||
resp = jsonify(d)
|
||||
return resp
|
||||
|
||||
|
||||
@bp.route("/course/<int:id>/students", methods=["GET"])
|
||||
@login_required
|
||||
def get_students_in_course(id):
|
||||
c = Course.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"course with id {id} not found")
|
||||
|
||||
students = c.students.filter_by(role="student")
|
||||
resp = {"students": []}
|
||||
for s in students:
|
||||
resp["students"].append(s.to_dict())
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route("/course/<int:id>/content", methods=["GET"])
|
||||
@login_required
|
||||
def get_content_in_course(id):
|
||||
c = Course.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"course with id {id} not found")
|
||||
|
||||
content = c.content.all()
|
||||
resp = {"content": []}
|
||||
for c in content:
|
||||
resp["content"].append(c.to_dict())
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route("/course/<int:id>/assignments", methods=["GET"])
|
||||
@login_required
|
||||
def get_assignments_in_course(id):
|
||||
c = Course.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"course with id {id} not found")
|
||||
|
||||
assignments = c.assignments.all()
|
||||
resp = {"assignments": []}
|
||||
for a in assignments:
|
||||
resp["assignments"].append(a.to_dict())
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route("/course/<int:id>", methods=["GET"])
|
||||
@login_required
|
||||
def get_course(id):
|
||||
c = Course.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"course with id {id} not found")
|
||||
resp = jsonify(c.to_dict())
|
||||
return resp
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>/enroll/<int:cid>", methods=["POST", "DELETE"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def enroll_student(uid, cid):
|
||||
u = User.query.get(uid)
|
||||
if not u:
|
||||
return error_response(400, f"User with id {uid} does not exist")
|
||||
|
||||
c = Course.query.get(cid)
|
||||
if not c:
|
||||
return error_response(400, f"Course with id {cid} does not exist")
|
||||
|
||||
if request.method == "POST":
|
||||
if not u.enroll(c):
|
||||
return error_response(
|
||||
400, f"User {uid} is already enrolled in course {cid}"
|
||||
)
|
||||
|
||||
elif request.method == "DELETE":
|
||||
if not u.unenroll(c):
|
||||
return error_response(400, f"User {uid} is not enrolled in course {cid}")
|
||||
|
||||
resp = {"user": u.to_dict(), "course": c.to_dict()}
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route("/user/<string:username>/enroll/<int:cid>", methods=["POST"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def enroll_student_by_username(username, cid):
|
||||
u = User.query.filter_by(username=username).first()
|
||||
if not u:
|
||||
return error_response(400, f"User with username {username} does not exist")
|
||||
|
||||
c = Course.query.get(cid)
|
||||
if not c:
|
||||
return error_response(400, f"Course with id {cid} does not exist")
|
||||
|
||||
if request.method == "POST":
|
||||
if not u.enroll(c):
|
||||
return error_response(
|
||||
400, f"User {u.id} is already enrolled in course {cid}"
|
||||
)
|
||||
|
||||
elif request.method == "DELETE":
|
||||
if not u.unenroll(c):
|
||||
return error_response(400, f"User {u.id} is not enrolled in course {cid}")
|
||||
|
||||
resp = {"user": u.to_dict(), "course": c.to_dict()}
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route("/assignment", methods=["POST"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def create_assignment():
|
||||
data = request.get_json()
|
||||
required_fields = ["name", "description", "course_id", "due_date"]
|
||||
if f := check_data(data, required_fields):
|
||||
return error_response(400, f"Must supply {f}")
|
||||
|
||||
c = Course.query.get(data["course_id"])
|
||||
if not c:
|
||||
return error_response(400, f"Course with id {data['course_id']} does not exist")
|
||||
|
||||
a = Assignment()
|
||||
a.from_dict(data)
|
||||
db.session.add(a)
|
||||
db.session.commit()
|
||||
return jsonify(a.to_dict())
|
||||
|
||||
|
||||
@bp.route("/assignment/<int:id>", methods=["GET"])
|
||||
@login_required
|
||||
def get_assignment(id):
|
||||
a = Assignment.query.get(id)
|
||||
if not a:
|
||||
return error_response(400, f"Assignment with id {id} does not exist")
|
||||
|
||||
return jsonify(a.to_dict())
|
||||
|
||||
|
||||
@bp.route("/assignment/<int:id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def delete_assignment(id):
|
||||
a = Assignment.query.get(id)
|
||||
if not a:
|
||||
return error_response(400, f"Assignment with id {id} does not exist")
|
||||
|
||||
db.session.delete(a)
|
||||
db.session.commit()
|
||||
return jsonify(a.to_dict())
|
||||
|
||||
|
||||
@bp.route("/assignment/<int:id>", methods=["PUT"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def update_assignment(id):
|
||||
a = Assignment.query.get(id)
|
||||
if not a:
|
||||
return error_response(400, f"Assignment with id {id} does not exist")
|
||||
|
||||
data = request.get_json()
|
||||
expected_fields = ["name", "description", "due_date"]
|
||||
for d in data:
|
||||
if d not in expected_fields:
|
||||
return error_response(400, f"Field {d} was not expected")
|
||||
|
||||
a.from_dict(data)
|
||||
db.session.commit()
|
||||
return jsonify(a.to_dict())
|
||||
|
||||
|
||||
@bp.route("/content", methods=["POST"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def create_content():
|
||||
data = request.get_json()
|
||||
required_fields = ["name", "body", "course_id"]
|
||||
if f := check_data(data, required_fields):
|
||||
return error_response(400, f"Must supply {f}")
|
||||
|
||||
c = Content()
|
||||
c.from_dict(data)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(c.to_dict())
|
||||
|
||||
|
||||
@bp.route("/content/<int:id>", methods=["GET"])
|
||||
@login_required
|
||||
def get_content(id):
|
||||
c = Content.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"Content with id {id} does not exist")
|
||||
|
||||
return jsonify(c.to_dict())
|
||||
|
||||
|
||||
@bp.route("/content/<int:id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def delete_content(id):
|
||||
c = Content.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"Content with id {id} does not exist")
|
||||
|
||||
db.session.delete(c)
|
||||
db.session.commit()
|
||||
return jsonify(c.to_dict())
|
||||
|
||||
|
||||
@bp.route("/content/<int:id>", methods=["PUT"])
|
||||
@login_required
|
||||
@instructor_required
|
||||
def update_content(id):
|
||||
c = Content.query.get(id)
|
||||
if not c:
|
||||
return error_response(400, f"Content with id {id} does not exist")
|
||||
|
||||
data = request.get_json()
|
||||
expected_fields = ["name", "body"]
|
||||
for d in data:
|
||||
if d not in expected_fields:
|
||||
return error_response(400, f"Field {d} was not expected")
|
||||
|
||||
c.from_dict(data)
|
||||
db.session.commit()
|
||||
return jsonify(c.to_dict())
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import dotenv
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
from app import create_app, db
|
||||
from app.models import User
|
||||
from app.models import Assignment, Course, User
|
||||
from flask_cors import CORS
|
||||
|
||||
app = create_app()
|
||||
CORS(app, supports_credentials=True, resources={r"/.*": {"origins": r".*localhost.*"}})
|
||||
CORS(
|
||||
app,
|
||||
supports_credentials=True,
|
||||
resources={r"/.*": {"origins": [r".*localhost.*", r".*jagrajaulakh.com.*"]}},
|
||||
)
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
return {"db": db, "User": User}
|
||||
return {"db": db, "User": User, "Course": Course, "Assignment": Assignment}
|
||||
|
||||
34
backend/migrations/versions/093a66f0b581_.py
Normal file
34
backend/migrations/versions/093a66f0b581_.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Create enrollment table
|
||||
|
||||
Revision ID: 093a66f0b581
|
||||
Revises: 471b4225837e
|
||||
Create Date: 2023-04-06 16:14:21.262823
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '093a66f0b581'
|
||||
down_revision = '471b4225837e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('enrollment',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('course_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('user_id', 'course_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('enrollment')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Added due date to assignment model
|
||||
|
||||
Revision ID: 1e88e783d238
|
||||
Revises: cab0d39ef662
|
||||
Create Date: 2023-04-13 18:33:37.785568
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1e88e783d238'
|
||||
down_revision = 'cab0d39ef662'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('assignment', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('assignment', schema=None) as batch_op:
|
||||
batch_op.drop_column('due_date')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
46
backend/migrations/versions/471b4225837e_.py
Normal file
46
backend/migrations/versions/471b4225837e_.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Create course model
|
||||
|
||||
Revision ID: 471b4225837e
|
||||
Revises: 8e48199f1417
|
||||
Create Date: 2023-04-06 14:53:43.061616
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '471b4225837e'
|
||||
down_revision = '8e48199f1417'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('course',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('instructor', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['instructor'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('course', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_course_description'), ['description'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_course_instructor'), ['instructor'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_course_name'), ['name'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('course', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_course_name'))
|
||||
batch_op.drop_index(batch_op.f('ix_course_instructor'))
|
||||
batch_op.drop_index(batch_op.f('ix_course_description'))
|
||||
|
||||
op.drop_table('course')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,4 +1,4 @@
|
||||
"""empty message
|
||||
"""Create user model
|
||||
|
||||
Revision ID: 7736bc740f9b
|
||||
Revises:
|
||||
|
||||
34
backend/migrations/versions/862905f5e34a_add_course_code.py
Normal file
34
backend/migrations/versions/862905f5e34a_add_course_code.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Add course code
|
||||
|
||||
Revision ID: 862905f5e34a
|
||||
Revises: 093a66f0b581
|
||||
Create Date: 2023-04-06 21:55:54.838647
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '862905f5e34a'
|
||||
down_revision = '093a66f0b581'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('course', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('course_code', sa.String(length=32), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_course_course_code'), ['course_code'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('course', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_course_course_code'))
|
||||
batch_op.drop_column('course_code')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,4 +1,4 @@
|
||||
"""empty message
|
||||
"""Add user role, remove about_me
|
||||
|
||||
Revision ID: 8e48199f1417
|
||||
Revises: 7736bc740f9b
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Create content model
|
||||
|
||||
Revision ID: c62aec1c6b91
|
||||
Revises: 1e88e783d238
|
||||
Create Date: 2023-04-14 16:46:43.513842
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c62aec1c6b91'
|
||||
down_revision = '1e88e783d238'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('content',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=True),
|
||||
sa.Column('body', sa.Text(), nullable=True),
|
||||
sa.Column('course_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('content', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_content_body'), ['body'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_content_course_id'), ['course_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_content_name'), ['name'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('content', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_content_name'))
|
||||
batch_op.drop_index(batch_op.f('ix_content_course_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_content_body'))
|
||||
|
||||
op.drop_table('content')
|
||||
# ### end Alembic commands ###
|
||||
46
backend/migrations/versions/cab0d39ef662_.py
Normal file
46
backend/migrations/versions/cab0d39ef662_.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Create assignment model
|
||||
|
||||
Revision ID: cab0d39ef662
|
||||
Revises: 862905f5e34a
|
||||
Create Date: 2023-04-13 18:27:15.748107
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'cab0d39ef662'
|
||||
down_revision = '862905f5e34a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('assignment',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=True),
|
||||
sa.Column('course_id', sa.Integer(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('assignment', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_assignment_course_id'), ['course_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_assignment_description'), ['description'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_assignment_name'), ['name'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('assignment', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_assignment_name'))
|
||||
batch_op.drop_index(batch_op.f('ix_assignment_description'))
|
||||
batch_op.drop_index(batch_op.f('ix_assignment_course_id'))
|
||||
|
||||
op.drop_table('assignment')
|
||||
# ### end Alembic commands ###
|
||||
@@ -12,6 +12,7 @@ itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
Mako==1.2.4
|
||||
MarkupSafe==2.1.2
|
||||
mysqlclient==2.1.1
|
||||
python-dotenv==1.0.0
|
||||
six==1.16.0
|
||||
SQLAlchemy==2.0.6
|
||||
|
||||
@@ -3,12 +3,31 @@ services:
|
||||
frontend:
|
||||
image: comp2707-frontend
|
||||
build: frontend/
|
||||
depends_on:
|
||||
- backend
|
||||
container_name: comp2707-frontend
|
||||
ports:
|
||||
- 8080:8080
|
||||
backend:
|
||||
image: comp2707-backend
|
||||
build: backend/
|
||||
depends_on:
|
||||
- db
|
||||
container_name: comp2707-backend
|
||||
environment:
|
||||
- DATABASE_URL=mysql://root:mama@db/2707
|
||||
ports:
|
||||
- 5000:5000
|
||||
- 5001:5000
|
||||
db:
|
||||
image: mariadb:latest
|
||||
environment:
|
||||
- MARIADB_ROOT_PASSWORD=mama
|
||||
- MARIADB_DATABASE=2707
|
||||
volumes:
|
||||
- db-volume:/var/lib/mysql
|
||||
ports:
|
||||
- 3406:3306
|
||||
|
||||
volumes:
|
||||
db-volume:
|
||||
driver: local
|
||||
|
||||
2
frontend/.dockerignore
Normal file
2
frontend/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.env.local
|
||||
node_modules/
|
||||
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
REACT_APP_BACKEND_URL=http://localhost:5000
|
||||
1
frontend/.env.production
Normal file
1
frontend/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
REACT_APP_BACKEND_URL=http://be.2707.jagrajaulakh.com:5001
|
||||
@@ -8,6 +8,7 @@ RUN mkdir /code && cp -a /tmp/node_modules /code/
|
||||
# Copy all the source code
|
||||
WORKDIR /code
|
||||
COPY ./ /code
|
||||
COPY .env.production .env
|
||||
|
||||
# Build the project
|
||||
RUN ["npm", "run", "build"]
|
||||
|
||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.7.2",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -6868,11 +6869,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-expand": {
|
||||
@@ -14577,6 +14578,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@@ -22349,9 +22358,9 @@
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
|
||||
},
|
||||
"dotenv-expand": {
|
||||
"version": "5.1.0",
|
||||
@@ -27743,6 +27752,13 @@
|
||||
"webpack-dev-server": "^4.6.0",
|
||||
"webpack-manifest-plugin": "^4.0.2",
|
||||
"workbox-webpack-plugin": "^6.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.7.2",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>LearningTree</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const app = express();
|
||||
const dotenv = require('dotenv');
|
||||
dotenv.config()
|
||||
console.log(process.env);
|
||||
|
||||
app.use("/*", (req, res, next) => {
|
||||
now = new Date();
|
||||
|
||||
@@ -4,31 +4,117 @@ import HomePage from "./pages/HomePage";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import LogoutPage from "./pages/LogoutPage";
|
||||
import RegisterPage from "./pages/RegisterPage";
|
||||
import CoursePage from "./pages/CoursePage";
|
||||
import AssignmentPage from "./pages/AssignmentPage";
|
||||
import ContentPage from "./pages/ContentPage";
|
||||
import ManagePage from "./pages/ManagePage";
|
||||
import ManageStudentsPage from "./pages/ManageStudentsPage";
|
||||
import AuthenticatedRoute from "./components/AuthenticatedRoute";
|
||||
import ManageAssignmentsPage from "./pages/ManageAssignmentsPage";
|
||||
import ManageContentPage from "./pages/ManageContentPage";
|
||||
import AssignmentEditPage from "./pages/AssignmentEditPage";
|
||||
|
||||
const AuthRoute = ({ isAuthenticated = true, path, children }) => {
|
||||
return (
|
||||
<Route path={path}>
|
||||
<AuthenticatedRoute isAuthenticated={isAuthenticated}>
|
||||
{children}
|
||||
</AuthenticatedRoute>
|
||||
</Route>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<Route path="/login">
|
||||
<AuthenticatedRoute isAuthenticated={false}>
|
||||
<AuthRoute path="/login" isAuthenticated={false}>
|
||||
<LoginPage />
|
||||
</AuthenticatedRoute>
|
||||
</Route>
|
||||
<Route path="/logout">
|
||||
<AuthenticatedRoute isAuthenticated={false}>
|
||||
</AuthRoute>
|
||||
<AuthRoute path="/logout" isAuthenticated={false}>
|
||||
<LogoutPage />
|
||||
</AuthenticatedRoute>
|
||||
</Route>
|
||||
<Route path="/register">
|
||||
<AuthenticatedRoute isAuthenticated={false}>
|
||||
</AuthRoute>
|
||||
<AuthRoute path="/register" isAuthenticated={false}>
|
||||
<RegisterPage />
|
||||
</AuthenticatedRoute>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<AuthenticatedRoute isAuthenticated={true}>
|
||||
</AuthRoute>
|
||||
|
||||
<AuthRoute path="/">
|
||||
<HomePage />
|
||||
</AuthRoute>
|
||||
|
||||
<Route path="/course/:id">
|
||||
{(params) => {
|
||||
return (
|
||||
<AuthenticatedRoute>
|
||||
<CoursePage id={params.id} />
|
||||
</AuthenticatedRoute>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
|
||||
<Route path="/content/:id">
|
||||
{(params) => {
|
||||
return (
|
||||
<AuthenticatedRoute>
|
||||
<ContentPage id={params.id} />
|
||||
</AuthenticatedRoute>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
|
||||
<Route path="/assignment/:id/edit">
|
||||
{(params) => {
|
||||
return (
|
||||
<AuthenticatedRoute>
|
||||
<AssignmentEditPage id={params.id} />
|
||||
</AuthenticatedRoute>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
|
||||
<Route path="/assignment/:id">
|
||||
{(params) => {
|
||||
return (
|
||||
<AuthenticatedRoute>
|
||||
<AssignmentPage id={params.id} />
|
||||
</AuthenticatedRoute>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
|
||||
<AuthRoute path="/manage">
|
||||
<ManagePage />
|
||||
</AuthRoute>
|
||||
|
||||
<Route path="/manage/:cid/students">
|
||||
{(params) => {
|
||||
return (
|
||||
<AuthenticatedRoute>
|
||||
<ManageStudentsPage cid={params.cid} />
|
||||
</AuthenticatedRoute>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
|
||||
<Route path="/manage/:id/content">
|
||||
{(params) => {
|
||||
return (
|
||||
<AuthenticatedRoute>
|
||||
<ManageContentPage cid={params.id} />
|
||||
</AuthenticatedRoute>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
|
||||
<Route path="/manage/:id/assignments">
|
||||
{(params) => {
|
||||
return (
|
||||
<AuthenticatedRoute>
|
||||
<ManageAssignmentsPage cid={params.id} />
|
||||
</AuthenticatedRoute>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
43
frontend/src/components/AssignmentsWidget.jsx
Normal file
43
frontend/src/components/AssignmentsWidget.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Card, Container } from "react-bootstrap";
|
||||
import { Link } from "wouter";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const AssignmentsWidget = ({ className = "", cid }) => {
|
||||
const [assignmentData, setAssignmentData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({ endpoint: `course/${cid}/assignments` })
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
setAssignmentData(data.assignments);
|
||||
});
|
||||
}, [setAssignmentData]);
|
||||
|
||||
return (
|
||||
<Container className={`${className} py-3 grid`}>
|
||||
<div className="row justify-content-center">
|
||||
{assignmentData.map((assignment, i) => {
|
||||
return (
|
||||
<Link
|
||||
is="a"
|
||||
key={i}
|
||||
href={`/assignment/${assignment.id}`}
|
||||
className="col col-lg-2"
|
||||
>
|
||||
<Card role="button" className="m-2" style={{ width: "300px" }}>
|
||||
<Card.Body>
|
||||
<Card.Title className="text-center">{assignment.name}</Card.Title>
|
||||
<Card.Text>Due: {assignment.due_date}</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignmentsWidget;
|
||||
@@ -2,7 +2,7 @@ import { useContext, useEffect } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
|
||||
const AuthenticatedRoute = ({ children, isAuthenticated }) => {
|
||||
const AuthenticatedRoute = ({ children, isAuthenticated=true }) => {
|
||||
const { currentUser } = useContext(UserContext);
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
@@ -12,7 +12,7 @@ const AuthenticatedRoute = ({ children, isAuthenticated }) => {
|
||||
} else if (!isAuthenticated && currentUser?.id) {
|
||||
setLocation("/");
|
||||
}
|
||||
}, [currentUser]);
|
||||
}, [currentUser, setLocation, isAuthenticated]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
42
frontend/src/components/ContentWidget.jsx
Normal file
42
frontend/src/components/ContentWidget.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Card, Container } from "react-bootstrap";
|
||||
import { Link } from "wouter";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const ContentWidget = ({ className = "", cid }) => {
|
||||
const [contentData, setContentData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({ endpoint: `course/${cid}/content` })
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
setContentData(data.content);
|
||||
});
|
||||
}, [setContentData]);
|
||||
|
||||
return (
|
||||
<Container className={`${className} py-3 grid`}>
|
||||
<div className="row justify-content-center">
|
||||
{contentData.map((content, i) => {
|
||||
return (
|
||||
<Link
|
||||
is="a"
|
||||
key={i}
|
||||
href={`/content/${content.id}`}
|
||||
className="col col-lg-2"
|
||||
>
|
||||
<Card role="button" className="m-2" style={{ width: "300px" }}>
|
||||
<Card.Body>
|
||||
<Card.Title className="text-center">{content.name}</Card.Title>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentWidget;
|
||||
50
frontend/src/components/CoursesWidget.jsx
Normal file
50
frontend/src/components/CoursesWidget.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Card, Container } from "react-bootstrap";
|
||||
import { Link } from "wouter";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const CoursesWidget = ({ className = "" }) => {
|
||||
const [courseData, setCourseData] = useState([]);
|
||||
const { currentUser } = useContext(UserContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser.id) {
|
||||
return;
|
||||
}
|
||||
makeRequest({ endpoint: `user/${currentUser.id}/courses` })
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
setCourseData(data.courses);
|
||||
});
|
||||
}, [setCourseData, currentUser]);
|
||||
|
||||
return (
|
||||
<Container className={`${className} py-3 grid`}>
|
||||
<div className="row justify-content-center">
|
||||
{courseData.map((course, i) => {
|
||||
return (
|
||||
<Link
|
||||
is="a"
|
||||
key={i}
|
||||
href={`/course/${course.id}`}
|
||||
className="col col-lg-2"
|
||||
>
|
||||
<Card role="button" className="m-2" style={{ width: "300px" }}>
|
||||
<h2 className="text-center py-5 border">
|
||||
{course.course_code}
|
||||
</h2>
|
||||
<Card.Body>
|
||||
<Card.Title>{course.name}</Card.Title>
|
||||
<Card.Text>{course.instructor}</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoursesWidget;
|
||||
@@ -6,27 +6,42 @@ import UserContext from "../contexts/UserContext";
|
||||
const MyNavbar = () => {
|
||||
const { currentUser } = useContext(UserContext);
|
||||
|
||||
const instructorLinks = [
|
||||
{
|
||||
label: "Manage Courses",
|
||||
link: "/manage",
|
||||
},
|
||||
];
|
||||
|
||||
const MyLink = ({ children, ...rest }) => {
|
||||
return <Nav.Link as={Link} {...rest}>{children}</Nav.Link>;
|
||||
return (
|
||||
<Nav.Link as={Link} {...rest}>
|
||||
{children}
|
||||
</Nav.Link>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Navbar variant="dark" bg="dark" expand="lg">
|
||||
<Container>
|
||||
<Navbar.Brand href="/">LearningTree</Navbar.Brand>
|
||||
<Navbar.Brand as={Link} href="/">
|
||||
LearningTree
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="navbar-nav" />
|
||||
<Navbar.Collapse id="navbar-nav">
|
||||
<Nav className="ms-auto">
|
||||
<MyLink href="/">Home</MyLink>
|
||||
{(currentUser?.id && (
|
||||
<MyLink href="/logout">Logout</MyLink>
|
||||
)) || <MyLink href="/login">Login</MyLink>}
|
||||
{(currentUser?.role === "instructor" || currentUser?.role === "admin") &&
|
||||
instructorLinks.map((item, k) => {
|
||||
return <MyLink key={k} href={item.link}>{item.label}</MyLink>;
|
||||
})}
|
||||
{(currentUser?.id && <MyLink href="/logout">Logout</MyLink>) || (
|
||||
<MyLink href="/login">Login</MyLink>
|
||||
)}
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
90
frontend/src/pages/AssignmentEditPage.jsx
Normal file
90
frontend/src/pages/AssignmentEditPage.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container, Table, Button, Form } from "react-bootstrap";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
const AssignmentEditPage = ({ id }) => {
|
||||
const [assignmentData, setAssignmentData] = useState({});
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [duedate, setDuedate] = useState("");
|
||||
|
||||
const submitAssignmentForm = (name, description, duedate) => {
|
||||
makeRequest({
|
||||
endpoint: `assignment/${id}`,
|
||||
method: "PUT",
|
||||
body: {
|
||||
name,
|
||||
description,
|
||||
due_date: duedate,
|
||||
},
|
||||
}).then((resp) => {
|
||||
setLocation(`/manage/${assignmentData.course_id}/assignments`);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({ endpoint: `assignment/${id}` })
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
setAssignmentData(data);
|
||||
setName(data.name);
|
||||
setDescription(data.description);
|
||||
setDuedate(data.due_date);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
{assignmentData.name && (
|
||||
<Form
|
||||
className="my-4 p-2 border"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitAssignmentForm(name, description, duedate);
|
||||
}}
|
||||
>
|
||||
<Form.Group controlId="assignmentName">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
value={name}
|
||||
type="text"
|
||||
placeholder="Enter assignment name"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="assignmentDescription">
|
||||
<Form.Label>Description</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={description}
|
||||
rows={4}
|
||||
placeholder="Enter assignment description"
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="assignmentDuedate">
|
||||
<Form.Label>Due Date</Form.Label>
|
||||
<Form.Control
|
||||
value={duedate}
|
||||
type="datetime-local"
|
||||
placeholder="Due date"
|
||||
onChange={(e) => setDuedate(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button className="my-2" type="submit" variant="success">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignmentEditPage;
|
||||
50
frontend/src/pages/AssignmentPage.jsx
Normal file
50
frontend/src/pages/AssignmentPage.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container } from "react-bootstrap";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const AssignmentPage = ({ id }) => {
|
||||
const [assignmentData, setAssignmentData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({ endpoint: `assignment/${id}` })
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
setAssignmentData(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const Title = ({ children, className, ...rest }) => {
|
||||
return (
|
||||
<p className={`${className} fs-6`} {...rest}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const Value = ({ children, className, ...rest }) => {
|
||||
return (
|
||||
<p className={`${className} fs-4`} {...rest}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
<Title>Name</Title>
|
||||
<Value>{assignmentData.name}</Value>
|
||||
<hr />
|
||||
<Title>Description</Title>
|
||||
<Value>{assignmentData.description}</Value>
|
||||
<hr />
|
||||
<Title>Due Date</Title>
|
||||
<Value>{assignmentData.due_date}</Value>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignmentPage;
|
||||
65
frontend/src/pages/ContentPage.jsx
Normal file
65
frontend/src/pages/ContentPage.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container } from "react-bootstrap";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const ContentPage = ({ id }) => {
|
||||
const [contentData, setContentData] = useState({});
|
||||
const [courseData, setCourseData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const wrap = async () => {
|
||||
let resp = await makeRequest({ endpoint: `content/${id}` });
|
||||
let data = await resp.json();
|
||||
setContentData(data);
|
||||
|
||||
resp = await makeRequest({ endpoint: `course/${data.course_id}` });
|
||||
data = await resp.json();
|
||||
setCourseData(data);
|
||||
};
|
||||
wrap();
|
||||
}, []);
|
||||
|
||||
const Title = ({ children, className, ...rest }) => {
|
||||
return (
|
||||
<p className={`${className} fs-6`} {...rest}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const Value = ({ children, className, ...rest }) => {
|
||||
return (
|
||||
<div className={`${className} fs-4`} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
<h1>{courseData.name}</h1>
|
||||
<hr />
|
||||
|
||||
<Container className="p-5 border">
|
||||
<Title>Name</Title>
|
||||
<Value>{contentData.name}</Value>
|
||||
<hr />
|
||||
<Title>Created</Title>
|
||||
<Value>{contentData.created_at}</Value>
|
||||
<hr />
|
||||
<Title>Body</Title>
|
||||
<Value>
|
||||
{contentData.body?.split("\n").map((line, k) => {
|
||||
return <div key={k}>{line}</div>;
|
||||
})}
|
||||
</Value>
|
||||
</Container>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentPage;
|
||||
38
frontend/src/pages/CoursePage.jsx
Normal file
38
frontend/src/pages/CoursePage.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container } from "react-bootstrap";
|
||||
import AssignmentsWidget from "../components/AssignmentsWidget";
|
||||
import ContentWidget from "../components/ContentWidget";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const CoursePage = ({ id }) => {
|
||||
const [courseData, setCourseData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({ endpoint: `course/${id}` })
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
setCourseData(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
<h1>{courseData.name}</h1>
|
||||
<h4 className="mb-4">{courseData.instructor}</h4>
|
||||
<hr />
|
||||
<div className="border">
|
||||
<h3>Course Content</h3>
|
||||
<ContentWidget cid={id} />
|
||||
<hr />
|
||||
<h3>Assignments</h3>
|
||||
<AssignmentsWidget cid={id} />
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoursePage;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useContext } from "react";
|
||||
import { Container } from "react-bootstrap";
|
||||
import CoursesWidget from "../components/CoursesWidget";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
|
||||
@@ -13,6 +14,13 @@ const HomePage = () => {
|
||||
<div>
|
||||
<h1>Welcome back {currentUser?.username}!</h1>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div>
|
||||
<h2>Courses</h2>
|
||||
<CoursesWidget className="ms-0 border" />
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Button, Col, Container, Form, Row, Alert } from "react-bootstrap";
|
||||
import { Link } from "wouter";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
import { sendLogoutRequest, sendLoginRequest } from "../utils.ts";
|
||||
|
||||
const LoginPage = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
@@ -11,32 +11,32 @@ const LoginPage = () => {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const { currentUser, setCurrentUser } = useContext(UserContext);
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser?.id) {
|
||||
gotoHome();
|
||||
setLocation("/");
|
||||
}
|
||||
}, [currentUser]);
|
||||
}, [currentUser, setLocation]);
|
||||
|
||||
const gotoHome = () => {
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
const sendLoginRequest = async (e) => {
|
||||
const formCb = async (e) => {
|
||||
e?.preventDefault();
|
||||
await makeRequest({
|
||||
url: "http://localhost:5000/login",
|
||||
method: "POST",
|
||||
body: { username, password },
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data);
|
||||
const res = await sendLoginRequest(username, password);
|
||||
|
||||
if (res?.isError) {
|
||||
if (res.message.includes("already")) {
|
||||
setError({
|
||||
...res,
|
||||
message: "An error occured. Try logging in again.",
|
||||
});
|
||||
await sendLogoutRequest();
|
||||
return;
|
||||
}
|
||||
setCurrentUser(data);
|
||||
});
|
||||
setError(res);
|
||||
return;
|
||||
}
|
||||
setCurrentUser(res);
|
||||
setLocation("/");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -52,7 +52,7 @@ const LoginPage = () => {
|
||||
)}
|
||||
<h2>Login</h2>
|
||||
<br />
|
||||
<Form onSubmit={sendLoginRequest}>
|
||||
<Form onSubmit={formCb}>
|
||||
<Form.Group as={Row} className="mb-3" controlId="username">
|
||||
<Form.Label column sm={2} className="me-2">
|
||||
Username
|
||||
|
||||
@@ -3,23 +3,20 @@ import { useContext, useEffect } from "react";
|
||||
import { Container } from "react-bootstrap";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
import { sendLogoutRequest } from "../utils.ts";
|
||||
|
||||
const LogoutPage = () => {
|
||||
const { setCurrentUser } = useContext(UserContext);
|
||||
useEffect(() => {
|
||||
const cleanup = async () => {
|
||||
await makeRequest({
|
||||
url: "http://localhost:5000/logout",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
await setCurrentUser({});
|
||||
(async () => {
|
||||
localStorage.removeItem("currentUser");
|
||||
try {
|
||||
await sendLogoutRequest();
|
||||
} catch (e) {}
|
||||
await setCurrentUser({});
|
||||
window.location.href = "/login";
|
||||
};
|
||||
cleanup();
|
||||
}, []);
|
||||
})();
|
||||
}, [setCurrentUser]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
||||
172
frontend/src/pages/ManageAssignmentsPage.jsx
Normal file
172
frontend/src/pages/ManageAssignmentsPage.jsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container, Table, Button, Form } from "react-bootstrap";
|
||||
import { useLocation } from "wouter";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const ManageAssignmentsPage = ({ cid }) => {
|
||||
const [courseData, setCourseData] = useState({});
|
||||
const [assignmentData, setAssignmentData] = useState([]);
|
||||
const [showAddAssignmentForm, setShowAddAssignmentForm] = useState(false);
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
const AddAssignmentForm = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [duedate, setDuedate] = useState("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
className="my-4 p-2 border"
|
||||
onSubmit={() => {
|
||||
submitAssignmentForm(name, description, duedate);
|
||||
}}
|
||||
>
|
||||
<Form.Group controlId="assignmentName">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter assignment name"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="assignmentDescription">
|
||||
<Form.Label>Description</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={4}
|
||||
placeholder="Enter assignment description"
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="assignmentDuedate">
|
||||
<Form.Label>Due Date</Form.Label>
|
||||
<Form.Control
|
||||
type="datetime-local"
|
||||
step={1}
|
||||
placeholder="Due date"
|
||||
onChange={(e) => setDuedate(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button className="my-2" type="submit" variant="success">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const submitAssignmentForm = (name, description, duedate) => {
|
||||
makeRequest({
|
||||
endpoint: "assignment",
|
||||
method: "POST",
|
||||
body: {
|
||||
name,
|
||||
description,
|
||||
course_id: cid,
|
||||
due_date: duedate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sendDeleteAssignmentRequest = (id) => {
|
||||
makeRequest({
|
||||
endpoint: `assignment/${id}`,
|
||||
method: "DELETE",
|
||||
}).then((resp) => {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({
|
||||
endpoint: `course/${cid}/assignments`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((req) => req.json())
|
||||
.then(async (data) => {
|
||||
setAssignmentData(data.assignments);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
makeRequest({
|
||||
endpoint: `course/${cid}`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((req) => req.json())
|
||||
.then(async (data) => {
|
||||
setCourseData(data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
<h1 className="mb-4">{courseData.name}</h1>
|
||||
<h3 className="mb-4">Manage Assignments</h3>
|
||||
<Button
|
||||
className="my-3"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setShowAddAssignmentForm(!showAddAssignmentForm);
|
||||
}}
|
||||
>
|
||||
{(showAddAssignmentForm && "-") || "+"} Add assignment
|
||||
</Button>
|
||||
{showAddAssignmentForm && <AddAssignmentForm />}
|
||||
<Table bordered striped hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Due date</th>
|
||||
<th>Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assignmentData.map((assignment, k) => {
|
||||
return (
|
||||
<tr key={k}>
|
||||
<td>{assignment.id}</td>
|
||||
<td>{assignment.name}</td>
|
||||
<td>{assignment.due_date}</td>
|
||||
<td>
|
||||
<div>
|
||||
<Button
|
||||
className="mx-1"
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
setLocation(`/assignment/${assignment.id}/edit`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
className="mx-1"
|
||||
variant="danger"
|
||||
onClick={() =>
|
||||
sendDeleteAssignmentRequest(assignment.id)
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageAssignmentsPage;
|
||||
149
frontend/src/pages/ManageContentPage.jsx
Normal file
149
frontend/src/pages/ManageContentPage.jsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container, Table, Button, Form } from "react-bootstrap";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const ManageContentPage = ({ cid }) => {
|
||||
const [courseData, setCourseData] = useState({});
|
||||
const [contentData, setContentData] = useState([]);
|
||||
const [showAddContentForm, setShowAddContentForm] = useState(false);
|
||||
|
||||
const AddContentForm = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
className="my-4 p-2 border"
|
||||
onSubmit={() => {
|
||||
submitContentForm(name, body);
|
||||
}}
|
||||
>
|
||||
<Form.Group controlId="contentName">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter content name"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="contentBody">
|
||||
<Form.Label>Body</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={4}
|
||||
placeholder="Enter content body"
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button className="my-2" type="submit" variant="success">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const submitContentForm = (name, body) => {
|
||||
makeRequest({
|
||||
endpoint: "content",
|
||||
method: "POST",
|
||||
body: {
|
||||
name,
|
||||
body,
|
||||
course_id: cid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sendDeleteContentRequest = (id) => {
|
||||
makeRequest({
|
||||
endpoint: `content/${id}`,
|
||||
method: "DELETE",
|
||||
}).then((resp) => {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({
|
||||
endpoint: `course/${cid}/content`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((req) => req.json())
|
||||
.then(async (data) => {
|
||||
setContentData(data.content);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
makeRequest({
|
||||
endpoint: `course/${cid}`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((req) => req.json())
|
||||
.then(async (data) => {
|
||||
setCourseData(data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
<h1 className="mb-4">{courseData.name}</h1>
|
||||
<h3 className="mb-4">Manage Course Content</h3>
|
||||
<Button
|
||||
className="my-3"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setShowAddContentForm(!showAddContentForm);
|
||||
}}
|
||||
>
|
||||
{(showAddContentForm && "-") || "+"} Add course content
|
||||
</Button>
|
||||
{showAddContentForm && <AddContentForm />}
|
||||
<Table bordered striped hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
<th>Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{contentData.map((content, k) => {
|
||||
return (
|
||||
<tr key={k}>
|
||||
<td>{content.id}</td>
|
||||
<td>{content.name}</td>
|
||||
<td>{content.created_at}</td>
|
||||
<td>
|
||||
<div>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() =>
|
||||
sendDeleteContentRequest(content.id)
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageContentPage;
|
||||
181
frontend/src/pages/ManagePage.jsx
Normal file
181
frontend/src/pages/ManagePage.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Form, Button, Container, Table } from "react-bootstrap";
|
||||
import { Link } from "wouter";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const ManagePage = () => {
|
||||
const [courseData, setCourseData] = useState([]);
|
||||
const { currentUser } = useContext(UserContext);
|
||||
const [showAddCourseForm, setShowAddCourseForm] = useState(false);
|
||||
|
||||
const AddCourseForm = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [coursecode, setCoursecode] = useState("");
|
||||
|
||||
const submitCourseForm = () => {
|
||||
makeRequest({
|
||||
endpoint: `course/${username}`,
|
||||
method: "POST",
|
||||
body: {
|
||||
course_code: coursecode,
|
||||
name,
|
||||
description,
|
||||
},
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
className="my-4 p-2 border justify-between items-center"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitCourseForm();
|
||||
}}
|
||||
>
|
||||
<Form.Group controlId="courseCode">
|
||||
<Form.Label>Course code</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter course code"
|
||||
onChange={(e) => setCoursecode(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="courseName">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter course name"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="courseDescription">
|
||||
<Form.Label>Description</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter course description"
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="courseInstructor">
|
||||
<Form.Label>Instructor Username</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Instructor"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button className="my-2" type="submit" variant="success">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const sendDeleteCourseRequest = (cid) => {
|
||||
makeRequest({
|
||||
endpoint: `course/${cid}`,
|
||||
method: "DELETE",
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({
|
||||
endpoint: `user/${currentUser.id}/courses`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((req) => req.json())
|
||||
.then((data) => {
|
||||
setCourseData(data.courses);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
<h1 className="mb-4">Manage Courses</h1>
|
||||
{currentUser.role === "admin" && (
|
||||
<Button
|
||||
className="my-3"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setShowAddCourseForm(!showAddCourseForm);
|
||||
}}
|
||||
>
|
||||
{(showAddCourseForm && "-") || "+"} Add course
|
||||
</Button>
|
||||
)}
|
||||
{showAddCourseForm && <AddCourseForm />}
|
||||
<Table bordered striped hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Course Code</th>
|
||||
<th>Name</th>
|
||||
<th>Instructor</th>
|
||||
<th>Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{courseData.map((course, k) => {
|
||||
return (
|
||||
<tr key={k}>
|
||||
<td>{course.id}</td>
|
||||
<td>{course.course_code}</td>
|
||||
<td>{course.name}</td>
|
||||
<td>{course.instructor}</td>
|
||||
<td>
|
||||
<div>
|
||||
<Link href={`/manage/${course.id}/students`}>
|
||||
Students
|
||||
</Link>
|
||||
<br />
|
||||
<Link href={`/manage/${course.id}/content`}>
|
||||
Course Content
|
||||
</Link>
|
||||
<br />
|
||||
<Link href={`/manage/${course.id}/assignments`}>
|
||||
Assignments
|
||||
</Link>
|
||||
{currentUser?.role === "admin" && (
|
||||
<React.Fragment>
|
||||
<br />
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => sendDeleteCourseRequest(course.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePage;
|
||||
123
frontend/src/pages/ManageStudentsPage.jsx
Normal file
123
frontend/src/pages/ManageStudentsPage.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container, Table, Button, Form } from "react-bootstrap";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
const ManageStutentsPage = ({ cid }) => {
|
||||
const [studentData, setStudentData] = useState([]);
|
||||
const [showAddStudentForm, setShowAddStudentForm] = useState(false);
|
||||
|
||||
const submitStudentForm = async (username) => {
|
||||
await makeRequest({
|
||||
endpoint: `user/${username}/enroll/${cid}`,
|
||||
method: "POST",
|
||||
});
|
||||
window.location.reload();
|
||||
return false;
|
||||
};
|
||||
|
||||
const AddStudentForm = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
className="my-4 p-2 border"
|
||||
onSubmit={() => {
|
||||
submitStudentForm(username);
|
||||
}}
|
||||
>
|
||||
<Form.Group controlId="studentUsername">
|
||||
<Form.Label>Username</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter student's username"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<Form.Text>
|
||||
Make sure that a user with the username exists and is not already
|
||||
enrolled in this course
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Button className="my-2" type="submit" variant="success">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
makeRequest({
|
||||
endpoint: `course/${cid}/students`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((req) => req.json())
|
||||
.then(async (data) => {
|
||||
setStudentData(data.students);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const sendUnenrollRequest = (uid) => {
|
||||
makeRequest({
|
||||
endpoint: `user/${uid}/enroll/${cid}`,
|
||||
method: "DELETE",
|
||||
}).then((resp) => {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyNavbar />
|
||||
<Container className="p-5 border">
|
||||
<h1 className="mb-4">Manage Students</h1>
|
||||
<Button
|
||||
className="my-3"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setShowAddStudentForm(!showAddStudentForm);
|
||||
}}
|
||||
>
|
||||
{(showAddStudentForm && "-") || "+"} Add student
|
||||
</Button>
|
||||
{showAddStudentForm && <AddStudentForm />}
|
||||
<Table bordered striped hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{studentData.map((student, k) => {
|
||||
return (
|
||||
<tr key={k}>
|
||||
<td>{student.id}</td>
|
||||
<td>{student.username}</td>
|
||||
<td>{student.email}</td>
|
||||
<td>
|
||||
<div>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => sendUnenrollRequest(student.id)}
|
||||
>
|
||||
Unenroll
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageStutentsPage;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Col, Container, Form, Row, Alert } from "react-bootstrap";
|
||||
import { Link } from "wouter";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import MyNavbar from "../components/MyNavbar";
|
||||
import { makeRequest } from "../utils.ts";
|
||||
|
||||
@@ -12,11 +12,12 @@ const RegisterPage = () => {
|
||||
const [password2, setPassword2] = useState("");
|
||||
|
||||
const [error, setError] = useState(null);
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
const sendRegisterRequest = (e) => {
|
||||
e?.preventDefault();
|
||||
makeRequest({
|
||||
url: "http://localhost:5000/register",
|
||||
endpoint: "register",
|
||||
method: "POST",
|
||||
body: {
|
||||
role,
|
||||
@@ -32,7 +33,7 @@ const RegisterPage = () => {
|
||||
setError(data);
|
||||
return;
|
||||
}
|
||||
window.location.href = "/login";
|
||||
setLocation("/login");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -61,7 +62,8 @@ const RegisterPage = () => {
|
||||
}}
|
||||
>
|
||||
<option value="student">Student</option>
|
||||
<option value="teacher">Teacher</option>
|
||||
<option value="instructor">Instructor</option>
|
||||
<option value="admin">Admin</option>
|
||||
</Form.Select>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
const makeRequest = ({ url, method, body }): Promise<Response> => {
|
||||
return fetch(url, {
|
||||
const { REACT_APP_BACKEND_URL } = process.env;
|
||||
|
||||
const makeRequest = ({ endpoint, method, body = null }): Promise<Response> => {
|
||||
const req: RequestInit = {
|
||||
method: method,
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body),
|
||||
headers: { "content-type": "application/json" },
|
||||
mode: "cors",
|
||||
});
|
||||
};
|
||||
if (body) {
|
||||
req["body"] = JSON.stringify(body);
|
||||
}
|
||||
return fetch(`${REACT_APP_BACKEND_URL}/${endpoint}`, req);
|
||||
};
|
||||
|
||||
export { makeRequest };
|
||||
const sendLoginRequest = async (
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<object> => {
|
||||
const p: Promise<object> = new Promise(async (res) => {
|
||||
await makeRequest({
|
||||
endpoint: "login",
|
||||
method: "POST",
|
||||
body: { username, password },
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
res({ isError: true, ...data });
|
||||
}
|
||||
res({ isError: false, ...data });
|
||||
});
|
||||
res({ isError: true });
|
||||
});
|
||||
return p;
|
||||
};
|
||||
|
||||
const sendLogoutRequest = async (): Promise<object> => {
|
||||
const p: Promise<object> = new Promise(async (res) => {
|
||||
await makeRequest({
|
||||
endpoint: "logout",
|
||||
method: "POST",
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
res({ isError: false, ...data });
|
||||
});
|
||||
res({ isError: true });
|
||||
});
|
||||
return p;
|
||||
};
|
||||
|
||||
export { makeRequest, sendLoginRequest, sendLogoutRequest };
|
||||
|
||||
Reference in New Issue
Block a user