Compare commits
2 Commits
master
...
0ba212e1bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
0ba212e1bb
|
|||
|
c72b49a92b
|
45
README.md
45
README.md
@@ -1,45 +0,0 @@
|
|||||||
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
|
COPY ./requirements.txt /code/requirements.txt
|
||||||
RUN . venv/bin/activate
|
RUN . venv/bin/activate
|
||||||
RUN python -m pip install -r /code/requirements.txt
|
RUN python -m pip install -r /code/requirements.txt
|
||||||
RUN cp -r venv /code/venv
|
COPY venv /code/venv
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
COPY app app
|
COPY app app
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class User(UserMixin, db.Model):
|
|||||||
"id": self.id,
|
"id": self.id,
|
||||||
"username": self.username,
|
"username": self.username,
|
||||||
"email": self.email,
|
"email": self.email,
|
||||||
"role": self.role,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def from_dict(self, data, new_user=False) -> None:
|
def from_dict(self, data, new_user=False) -> None:
|
||||||
@@ -76,8 +75,6 @@ class Course(db.Model):
|
|||||||
description = sa.Column(sa.Text, index=True)
|
description = sa.Column(sa.Text, index=True)
|
||||||
instructor = sa.Column(sa.ForeignKey(User.id), index=True)
|
instructor = sa.Column(sa.ForeignKey(User.id), index=True)
|
||||||
created_at = sa.Column(sa.DateTime)
|
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:
|
def __repr__(self) -> str:
|
||||||
return f"<Course {self.course_code}>"
|
return f"<Course {self.course_code}>"
|
||||||
@@ -97,51 +94,3 @@ class Course(db.Model):
|
|||||||
|
|
||||||
d["instructor"] = User.query.get(self.instructor).username
|
d["instructor"] = User.query.get(self.instructor).username
|
||||||
return d
|
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,4 +1,3 @@
|
|||||||
from functools import wraps
|
|
||||||
from flask_login import login_required, login_user, logout_user
|
from flask_login import login_required, login_user, logout_user
|
||||||
from app.bp import bp
|
from app.bp import bp
|
||||||
from flask import jsonify, request
|
from flask import jsonify, request
|
||||||
@@ -6,7 +5,7 @@ from app.errors import error_response
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import login, db
|
from app import login, db
|
||||||
from app.models import Content, Course, User, Assignment
|
from app.models import Course, User
|
||||||
|
|
||||||
|
|
||||||
@login.user_loader
|
@login.user_loader
|
||||||
@@ -21,26 +20,6 @@ def check_data(data, required_fields):
|
|||||||
return None
|
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"])
|
@bp.route("/login", methods=["POST"])
|
||||||
def login_route():
|
def login_route():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -95,7 +74,6 @@ def register():
|
|||||||
|
|
||||||
@bp.route("/course", methods=["POST"])
|
@bp.route("/course", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
|
||||||
def create_course():
|
def create_course():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
@@ -110,136 +88,32 @@ def create_course():
|
|||||||
|
|
||||||
c = Course.query.filter_by(course_code=data["course_code"]).first()
|
c = Course.query.filter_by(course_code=data["course_code"]).first()
|
||||||
if c:
|
if c:
|
||||||
return error_response(
|
return error_response(400, f"Course with course code {data['course_code']} already exists")
|
||||||
400, f"Course with course code {data['course_code']} already exists"
|
|
||||||
)
|
|
||||||
|
|
||||||
if u.role != "instructor":
|
if u.role != "instructor":
|
||||||
return error_response(400, "User is not instructor")
|
return error_response(400, "User is not instructor")
|
||||||
|
|
||||||
c = Course()
|
c = Course()
|
||||||
c.from_dict(data)
|
c.from_dict(data)
|
||||||
u.enroll(c)
|
|
||||||
db.session.add(c)
|
db.session.add(c)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(c.to_dict())
|
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"])
|
@bp.route("/user/<int:id>/courses", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_courses(id):
|
def get_courses(id):
|
||||||
u = User.query.get(id)
|
u = User.query.get(id)
|
||||||
d = {"courses": []}
|
d = {"courses": []}
|
||||||
courses = Course.query.all() if u.role == "admin" else u.enrolled_courses.all()
|
for c in u.enrolled_courses.all():
|
||||||
for c in courses:
|
|
||||||
d["courses"].append(c.to_dict())
|
d["courses"].append(c.to_dict())
|
||||||
resp = jsonify(d)
|
resp = jsonify(d)
|
||||||
return resp
|
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"])
|
@bp.route("/user/<int:uid>/enroll/<int:cid>", methods=["POST", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
@instructor_required
|
|
||||||
def enroll_student(uid, cid):
|
def enroll_student(uid, cid):
|
||||||
u = User.query.get(uid)
|
u = User.query.get(uid)
|
||||||
if not u:
|
if not u:
|
||||||
@@ -251,9 +125,7 @@ def enroll_student(uid, cid):
|
|||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if not u.enroll(c):
|
if not u.enroll(c):
|
||||||
return error_response(
|
return error_response(400, f"User {uid} is already enrolled in course {cid}")
|
||||||
400, f"User {uid} is already enrolled in course {cid}"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif request.method == "DELETE":
|
elif request.method == "DELETE":
|
||||||
if not u.unenroll(c):
|
if not u.unenroll(c):
|
||||||
@@ -262,149 +134,3 @@ def enroll_student(uid, cid):
|
|||||||
resp = {"user": u.to_dict(), "course": c.to_dict()}
|
resp = {"user": u.to_dict(), "course": c.to_dict()}
|
||||||
return jsonify(resp)
|
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())
|
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ import dotenv
|
|||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
from app import create_app, db
|
from app import create_app, db
|
||||||
from app.models import Assignment, Course, User
|
from app.models import Course, User
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
CORS(
|
CORS(app, supports_credentials=True, resources={r"/.*": {"origins": r".*localhost.*"}})
|
||||||
app,
|
|
||||||
supports_credentials=True,
|
|
||||||
resources={r"/.*": {"origins": [r".*localhost.*", r".*jagrajaulakh.com.*"]}},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.shell_context_processor
|
@app.shell_context_processor
|
||||||
def make_shell_context():
|
def make_shell_context():
|
||||||
return {"db": db, "User": User, "Course": Course, "Assignment": Assignment}
|
return {"db": db, "User": User, "Course": Course}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@@ -3,31 +3,12 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
image: comp2707-frontend
|
image: comp2707-frontend
|
||||||
build: frontend/
|
build: frontend/
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
container_name: comp2707-frontend
|
container_name: comp2707-frontend
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
backend:
|
backend:
|
||||||
image: comp2707-backend
|
image: comp2707-backend
|
||||||
build: backend/
|
build: backend/
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
container_name: comp2707-backend
|
container_name: comp2707-backend
|
||||||
environment:
|
|
||||||
- DATABASE_URL=mysql://root:mama@db/2707
|
|
||||||
ports:
|
ports:
|
||||||
- 5001:5000
|
- 5000: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
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.env.local
|
|
||||||
node_modules/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
REACT_APP_BACKEND_URL=http://localhost:5000
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
REACT_APP_BACKEND_URL=http://be.2707.jagrajaulakh.com:5001
|
|
||||||
@@ -8,7 +8,6 @@ RUN mkdir /code && cp -a /tmp/node_modules /code/
|
|||||||
# Copy all the source code
|
# Copy all the source code
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
COPY ./ /code
|
COPY ./ /code
|
||||||
COPY .env.production .env
|
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
RUN ["npm", "run", "build"]
|
RUN ["npm", "run", "build"]
|
||||||
|
|||||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@@ -12,7 +12,6 @@
|
|||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"bootstrap": "^5.2.3",
|
"bootstrap": "^5.2.3",
|
||||||
"dotenv": "^16.0.3",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.7.2",
|
"react-bootstrap": "^2.7.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -6869,11 +6868,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.0.3",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv-expand": {
|
"node_modules/dotenv-expand": {
|
||||||
@@ -14578,14 +14577,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
@@ -22358,9 +22349,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dotenv": {
|
"dotenv": {
|
||||||
"version": "16.0.3",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
|
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
|
||||||
},
|
},
|
||||||
"dotenv-expand": {
|
"dotenv-expand": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
@@ -27752,13 +27743,6 @@
|
|||||||
"webpack-dev-server": "^4.6.0",
|
"webpack-dev-server": "^4.6.0",
|
||||||
"webpack-manifest-plugin": "^4.0.2",
|
"webpack-manifest-plugin": "^4.0.2",
|
||||||
"workbox-webpack-plugin": "^6.4.1"
|
"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": {
|
"react-transition-group": {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"bootstrap": "^5.2.3",
|
"bootstrap": "^5.2.3",
|
||||||
"dotenv": "^16.0.3",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.7.2",
|
"react-bootstrap": "^2.7.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const app = express();
|
const app = express();
|
||||||
const dotenv = require('dotenv');
|
|
||||||
dotenv.config()
|
|
||||||
console.log(process.env);
|
|
||||||
|
|
||||||
app.use("/*", (req, res, next) => {
|
app.use("/*", (req, res, next) => {
|
||||||
now = new Date();
|
now = new Date();
|
||||||
|
|||||||
@@ -4,117 +4,31 @@ import HomePage from "./pages/HomePage";
|
|||||||
import LoginPage from "./pages/LoginPage";
|
import LoginPage from "./pages/LoginPage";
|
||||||
import LogoutPage from "./pages/LogoutPage";
|
import LogoutPage from "./pages/LogoutPage";
|
||||||
import RegisterPage from "./pages/RegisterPage";
|
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 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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<AuthRoute path="/login" isAuthenticated={false}>
|
<Route path="/login">
|
||||||
|
<AuthenticatedRoute isAuthenticated={false}>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AuthRoute>
|
</AuthenticatedRoute>
|
||||||
<AuthRoute path="/logout" isAuthenticated={false}>
|
</Route>
|
||||||
|
<Route path="/logout">
|
||||||
|
<AuthenticatedRoute isAuthenticated={false}>
|
||||||
<LogoutPage />
|
<LogoutPage />
|
||||||
</AuthRoute>
|
</AuthenticatedRoute>
|
||||||
<AuthRoute path="/register" isAuthenticated={false}>
|
</Route>
|
||||||
|
<Route path="/register">
|
||||||
|
<AuthenticatedRoute isAuthenticated={false}>
|
||||||
<RegisterPage />
|
<RegisterPage />
|
||||||
</AuthRoute>
|
</AuthenticatedRoute>
|
||||||
|
</Route>
|
||||||
<AuthRoute path="/">
|
<Route path="/">
|
||||||
|
<AuthenticatedRoute isAuthenticated={true}>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</AuthRoute>
|
|
||||||
|
|
||||||
<Route path="/course/:id">
|
|
||||||
{(params) => {
|
|
||||||
return (
|
|
||||||
<AuthenticatedRoute>
|
|
||||||
<CoursePage id={params.id} />
|
|
||||||
</AuthenticatedRoute>
|
</AuthenticatedRoute>
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Route>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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 { useLocation } from "wouter";
|
||||||
import UserContext from "../contexts/UserContext";
|
import UserContext from "../contexts/UserContext";
|
||||||
|
|
||||||
const AuthenticatedRoute = ({ children, isAuthenticated=true }) => {
|
const AuthenticatedRoute = ({ children, isAuthenticated }) => {
|
||||||
const { currentUser } = useContext(UserContext);
|
const { currentUser } = useContext(UserContext);
|
||||||
const [location, setLocation] = useLocation();
|
const [location, setLocation] = useLocation();
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -9,10 +9,7 @@ const CoursesWidget = ({ className = "" }) => {
|
|||||||
const { currentUser } = useContext(UserContext);
|
const { currentUser } = useContext(UserContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser.id) {
|
makeRequest({ url: `http://localhost:5000/user/${currentUser.id}/courses` })
|
||||||
return;
|
|
||||||
}
|
|
||||||
makeRequest({ endpoint: `user/${currentUser.id}/courses` })
|
|
||||||
.then((resp) => resp.json())
|
.then((resp) => resp.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setCourseData(data.courses);
|
setCourseData(data.courses);
|
||||||
@@ -27,13 +24,11 @@ const CoursesWidget = ({ className = "" }) => {
|
|||||||
<Link
|
<Link
|
||||||
is="a"
|
is="a"
|
||||||
key={i}
|
key={i}
|
||||||
href={`/course/${course.id}`}
|
href={`/course/${course.course_id}`}
|
||||||
className="col col-lg-2"
|
className="col col-lg-2"
|
||||||
>
|
>
|
||||||
<Card role="button" className="m-2" style={{ width: "300px" }}>
|
<Card role="button" className="m-2" style={{ width: "300px" }}>
|
||||||
<h2 className="text-center py-5 border">
|
<h2 className="text-center py-5 border">{course.course_code}</h2>
|
||||||
{course.course_code}
|
|
||||||
</h2>
|
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<Card.Title>{course.name}</Card.Title>
|
<Card.Title>{course.name}</Card.Title>
|
||||||
<Card.Text>{course.instructor}</Card.Text>
|
<Card.Text>{course.instructor}</Card.Text>
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ import UserContext from "../contexts/UserContext";
|
|||||||
const MyNavbar = () => {
|
const MyNavbar = () => {
|
||||||
const { currentUser } = useContext(UserContext);
|
const { currentUser } = useContext(UserContext);
|
||||||
|
|
||||||
const instructorLinks = [
|
|
||||||
{
|
|
||||||
label: "Manage Courses",
|
|
||||||
link: "/manage",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const MyLink = ({ children, ...rest }) => {
|
const MyLink = ({ children, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
<Nav.Link as={Link} {...rest}>
|
<Nav.Link as={Link} {...rest}>
|
||||||
@@ -24,17 +17,11 @@ const MyNavbar = () => {
|
|||||||
return (
|
return (
|
||||||
<Navbar variant="dark" bg="dark" expand="lg">
|
<Navbar variant="dark" bg="dark" expand="lg">
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar.Brand as={Link} href="/">
|
<Navbar.Brand as={Link} href="/">LearningTree</Navbar.Brand>
|
||||||
LearningTree
|
|
||||||
</Navbar.Brand>
|
|
||||||
<Navbar.Toggle aria-controls="navbar-nav" />
|
<Navbar.Toggle aria-controls="navbar-nav" />
|
||||||
<Navbar.Collapse id="navbar-nav">
|
<Navbar.Collapse id="navbar-nav">
|
||||||
<Nav className="ms-auto">
|
<Nav className="ms-auto">
|
||||||
<MyLink href="/">Home</MyLink>
|
<MyLink href="/">Home</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>) || (
|
{(currentUser?.id && <MyLink href="/logout">Logout</MyLink>) || (
|
||||||
<MyLink href="/login">Login</MyLink>
|
<MyLink href="/login">Login</MyLink>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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,172 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -17,7 +17,7 @@ const RegisterPage = () => {
|
|||||||
const sendRegisterRequest = (e) => {
|
const sendRegisterRequest = (e) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
makeRequest({
|
makeRequest({
|
||||||
endpoint: "register",
|
url: "http://localhost:5000/register",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
role,
|
role,
|
||||||
@@ -62,8 +62,7 @@ const RegisterPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="student">Student</option>
|
<option value="student">Student</option>
|
||||||
<option value="instructor">Instructor</option>
|
<option value="teacher">Teacher</option>
|
||||||
<option value="admin">Admin</option>
|
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
const { REACT_APP_BACKEND_URL } = process.env;
|
const makeRequest = ({ url, method, body = null }): Promise<Response> => {
|
||||||
|
|
||||||
const makeRequest = ({ endpoint, method, body = null }): Promise<Response> => {
|
|
||||||
const req: RequestInit = {
|
const req: RequestInit = {
|
||||||
method: method,
|
method: method,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -10,7 +8,7 @@ const makeRequest = ({ endpoint, method, body = null }): Promise<Response> => {
|
|||||||
if (body) {
|
if (body) {
|
||||||
req["body"] = JSON.stringify(body);
|
req["body"] = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
return fetch(`${REACT_APP_BACKEND_URL}/${endpoint}`, req);
|
return fetch(url, req);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendLoginRequest = async (
|
const sendLoginRequest = async (
|
||||||
@@ -19,7 +17,7 @@ const sendLoginRequest = async (
|
|||||||
): Promise<object> => {
|
): Promise<object> => {
|
||||||
const p: Promise<object> = new Promise(async (res) => {
|
const p: Promise<object> = new Promise(async (res) => {
|
||||||
await makeRequest({
|
await makeRequest({
|
||||||
endpoint: "login",
|
url: "http://localhost:5000/login",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { username, password },
|
body: { username, password },
|
||||||
})
|
})
|
||||||
@@ -38,7 +36,7 @@ const sendLoginRequest = async (
|
|||||||
const sendLogoutRequest = async (): Promise<object> => {
|
const sendLogoutRequest = async (): Promise<object> => {
|
||||||
const p: Promise<object> = new Promise(async (res) => {
|
const p: Promise<object> = new Promise(async (res) => {
|
||||||
await makeRequest({
|
await makeRequest({
|
||||||
endpoint: "logout",
|
url: "http://localhost:5000/logout",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
})
|
||||||
.then((resp) => resp.json())
|
.then((resp) => resp.json())
|
||||||
|
|||||||
Reference in New Issue
Block a user