Compare commits

...

42 Commits

Author SHA1 Message Date
a56621d6ae Add README 2023-04-14 21:24:36 -04:00
3bbeed57ed #58 add admin logic 2023-04-14 19:03:54 -04:00
97e958697f #76 Added course content page 2023-04-14 18:31:04 -04:00
035ab9a6c7 #76 Made content widget 2023-04-14 18:13:59 -04:00
b1008a63d9 #66 Add edit button that takes user to assignment edit page 2023-04-14 17:58:09 -04:00
dccc1949e0 #47 Made manage content page 2023-04-14 17:06:25 -04:00
8626fd9a44 #70 Add endpoint to update content 2023-04-14 17:02:01 -04:00
cab68b147c #70 Add endpoint to delete content 2023-04-14 17:00:10 -04:00
0ea4e3933f #70 Add endpoint to get content by id 2023-04-14 16:57:58 -04:00
608854ebb0 #70 Add endpoint to get all content in a specific course 2023-04-14 16:56:25 -04:00
245b7bbb2a #70 Add endpoint to create content 2023-04-14 16:54:34 -04:00
80ccc17eef #70 Add content model 2023-04-14 16:47:27 -04:00
b03c50445f #67 Made endpoint to update assignment fields 2023-04-14 16:37:04 -04:00
35ddecb5a9 #65 endpoint to delete assignment 2023-04-14 16:12:52 -04:00
35e8b2eec3 #46 Manage assignments page shows table of assignments for a specific course. 2023-04-14 16:06:47 -04:00
ecbbcb04c0 #62 Made assignment page, showing name, description, and due date 2023-04-14 15:11:27 -04:00
ddf8fd57a0 #61 Added assignment widget on course page 2023-04-14 14:59:14 -04:00
9be7abcd05 #53 Endpoint to get an assignment by id 2023-04-14 14:48:14 -04:00
27d2317e84 #57 CORS allow production url 2023-04-14 14:42:30 -04:00
0740f1f4ac #57 Use production env for docker 2023-04-14 14:30:53 -04:00
f513c31664 #57 Production backend url 2023-04-14 14:20:15 -04:00
c7797c9401 #57 Copy venv from container's cwd, not host 2023-04-14 14:02:10 -04:00
275f0f0914 #57 Added frontend env files for local and production. 2023-04-14 13:55:34 -04:00
c1a8f04dc7 #57 makeRequest uses env variable so all requests use the configuration 2023-04-14 12:55:43 -04:00
1f0e8ca905 #52 Create endpoint to get a course's assignments 2023-04-13 18:49:47 -04:00
389f71c2f8 #10 Made endpoint to create a new assignment 2023-04-13 18:46:54 -04:00
2b071d35c8 #10 Add due date column to assignment model 2023-04-13 18:35:51 -04:00
104370fb79 #10 Create assignment model 2023-04-13 18:32:09 -04:00
8670612259 Cleanup routes with AuthRoute 2023-04-13 18:18:30 -04:00
29e417c9d0 #45 Add student form, unenroll button 2023-04-13 18:07:06 -04:00
a6ae87abf2 #45 Added backend route to enroll student by username 2023-04-13 18:05:43 -04:00
4faecbd29c #45 Made manage student page. Doesn't call backend yet 2023-04-13 16:52:28 -04:00
542e8c232a #49 Add endpoint to get the students enrolled in a specific course 2023-04-13 16:45:39 -04:00
f0b101386b #44 Made manage page. Displays table of all courses that the instuctor is teaching 2023-04-13 16:11:33 -04:00
e07b5add79 #38 Made instructor specific links on the navbar 2023-04-13 15:02:41 -04:00
0db9844e77 Add instructor as an enrolled user to a course. Also change frontend to use instructor role instead of teacher 2023-04-13 14:51:40 -04:00
797b7235e9 Course widget now uses correct id field to navigate to course page 2023-04-13 14:38:04 -04:00
a2bc3567c5 #39 Added endpoint to get a specific course by id 2023-04-13 14:11:18 -04:00
24f6256703 #31 Added course page that querys backend for a specific course id 2023-04-13 14:10:26 -04:00
1dfa9c1421 #32 Remove dummy data. Modified makeRequest so it doesn't send body on GET 2023-04-06 23:10:38 -04:00
15d336ca53 #32 Fetch enrolled course data from backend and store the data 2023-04-06 23:10:38 -04:00
9c6b46f68f Add decorator that requirers the logged in user to be an instructor 2023-04-06 23:08:18 -04:00
32 changed files with 1664 additions and 77 deletions

45
README.md Normal file
View 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
```

View File

@@ -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

View File

@@ -58,6 +58,7 @@ class User(UserMixin, db.Model):
"id": self.id,
"username": self.username,
"email": self.email,
"role": self.role,
}
def from_dict(self, data, new_user=False) -> None:
@@ -75,6 +76,8 @@ class Course(db.Model):
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}>"
@@ -94,3 +97,51 @@ class Course(db.Model):
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

View File

@@ -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 Course, User
from app.models import Content, Course, User, Assignment
@login.user_loader
@@ -20,6 +21,26 @@ def check_data(data, required_fields):
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()
@@ -74,6 +95,7 @@ def register():
@bp.route("/course", methods=["POST"])
@login_required
@admin_required
def create_course():
data = request.get_json()
@@ -88,32 +110,136 @@ def create_course():
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")
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": []}
for c in u.enrolled_courses.all():
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:
@@ -125,7 +251,9 @@ def enroll_student(uid, cid):
if request.method == "POST":
if not u.enroll(c):
return error_response(400, f"User {uid} is already enrolled in course {cid}")
return error_response(
400, f"User {uid} is already enrolled in course {cid}"
)
elif request.method == "DELETE":
if not u.unenroll(c):
@@ -134,3 +262,149 @@ def enroll_student(uid, 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())

View File

@@ -3,13 +3,17 @@ import dotenv
dotenv.load_dotenv()
from app import create_app, db
from app.models import Course, 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, "Course": Course}
return {"db": db, "User": User, "Course": Course, "Assignment": Assignment}

View File

@@ -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 ###

View File

@@ -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 ###

View 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 ###

View File

@@ -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
View File

@@ -0,0 +1,2 @@
.env.local
node_modules/

1
frontend/.env Normal file
View File

@@ -0,0 +1 @@
REACT_APP_BACKEND_URL=http://localhost:5000

1
frontend/.env.production Normal file
View File

@@ -0,0 +1 @@
REACT_APP_BACKEND_URL=http://be.2707.jagrajaulakh.com:5001

View File

@@ -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"]

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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();

View File

@@ -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 />
</AuthenticatedRoute>
</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>
);
}

View 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;

View File

@@ -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();

View 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;

View File

@@ -1,54 +1,41 @@
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 dummyData = [
{
course_id: 1,
course_title: "Advanced Website Design",
couse_code: "COMP 2707",
instructor: "Saja Al Mamoori",
},
{
course_id: 2,
course_title: "Introduction to Roman Civilization",
couse_code: "GRST 1200",
instructor: "Max Nelson",
},
{
course_id: 3,
course_title: "Software Verification and Testing",
couse_code: "COMP 4110",
instructor: "Serif Saad",
},
{
course_id: 4,
course_title: "Selected Topics in Software Engineering",
couse_code: "COMP 4800",
instructor: "Jessica Chen",
},
{
course_id: 5,
course_title: "Project Management: Techniques and Tools",
couse_code: "COMP 4990",
instructor: "Arunita Jaekel",
},
];
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">
{dummyData.map((course, i) => {
{courseData.map((course, i) => {
return (
<Link
is="a"
key={i}
href={`/course/${course.course_id}`}
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.couse_code}</h2>
<h2 className="text-center py-5 border">
{course.course_code}
</h2>
<Card.Body>
<Card.Title>{course.course_title}</Card.Title>
<Card.Title>{course.name}</Card.Title>
<Card.Text>{course.instructor}</Card.Text>
</Card.Body>
</Card>

View File

@@ -6,6 +6,13 @@ 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}>
@@ -17,11 +24,17 @@ const MyNavbar = () => {
return (
<Navbar variant="dark" bg="dark" expand="lg">
<Container>
<Navbar.Brand as={Link} 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?.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>
)}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -17,7 +17,7 @@ const RegisterPage = () => {
const sendRegisterRequest = (e) => {
e?.preventDefault();
makeRequest({
url: "http://localhost:5000/register",
endpoint: "register",
method: "POST",
body: {
role,
@@ -62,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>

View File

@@ -1,11 +1,16 @@
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);
};
const sendLoginRequest = async (
@@ -14,7 +19,7 @@ const sendLoginRequest = async (
): Promise<object> => {
const p: Promise<object> = new Promise(async (res) => {
await makeRequest({
url: "http://localhost:5000/login",
endpoint: "login",
method: "POST",
body: { username, password },
})
@@ -33,7 +38,7 @@ const sendLoginRequest = async (
const sendLogoutRequest = async (): Promise<object> => {
const p: Promise<object> = new Promise(async (res) => {
await makeRequest({
url: "http://localhost:5000/logout",
endpoint: "logout",
method: "POST",
})
.then((resp) => resp.json())