diff --git a/backend/app/models.py b/backend/app/models.py index 3431fc8..d91c295 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -42,6 +42,14 @@ class User(UserMixin, db.Model): def enroll(self, c) -> bool: if not self.is_enrolled(c): self.enrolled_courses.append(c) + db.session.commit() + return True + return False + + def unenroll(self, c) -> bool: + if self.is_enrolled(c): + self.enrolled_courses.remove(c) + db.session.commit() return True return False @@ -63,15 +71,16 @@ class User(UserMixin, db.Model): class Course(db.Model): id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String(128), index=True) + course_code = sa.Column(sa.String(32), index=True) description = sa.Column(sa.Text, index=True) instructor = sa.Column(sa.ForeignKey(User.id), index=True) created_at = sa.Column(sa.DateTime) def __repr__(self) -> str: - return f"" + return f"" def from_dict(self, data) -> None: - for field in ["name", "description", "instructor"]: + for field in ["name", "course_code", "description", "instructor"]: if field in data: setattr(self, field, data[field]) @@ -80,7 +89,7 @@ class Course(db.Model): def to_dict(self) -> dict: d = {} - for f in ["id", "name", "description", "created_at"]: + for f in ["id", "name", "course_code", "description", "created_at"]: d[f] = getattr(self, f) d["instructor"] = User.query.get(self.instructor).username diff --git a/backend/app/routes.py b/backend/app/routes.py index fd86958..89dfa1e 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -73,10 +73,11 @@ def register(): @bp.route("/course", methods=["POST"]) +@login_required def create_course(): data = request.get_json() - required_fields = ["name", "description", "instructor"] + required_fields = ["name", "course_code", "description", "instructor"] if f := check_data(data, required_fields): return error_response(400, f"Must supply {f}") @@ -85,6 +86,10 @@ def create_course(): if not u: return error_response(400, f"User with id {data['instructor']} does not exist") + c = Course.query.filter_by(course_code=data["course_code"]).first() + if c: + return error_response(400, f"Course with course code {data['course_code']} already exists") + if u.role != "instructor": return error_response(400, "User is not instructor") @@ -97,6 +102,7 @@ def create_course(): @bp.route("/user//courses", methods=["GET"]) +@login_required def get_courses(id): u = User.query.get(id) d = {"courses": []} @@ -104,3 +110,27 @@ def get_courses(id): d["courses"].append(c.to_dict()) resp = jsonify(d) return resp + + +@bp.route("/user//enroll/", methods=["POST", "DELETE"]) +@login_required +def enroll_student(uid, cid): + u = User.query.get(uid) + if not u: + return error_response(400, f"User with id {uid} does not exist") + + c = Course.query.get(cid) + if not c: + return error_response(400, f"Course with id {cid} does not exist") + + if request.method == "POST": + if not u.enroll(c): + return error_response(400, f"User {uid} is already enrolled in course {cid}") + + elif request.method == "DELETE": + if not u.unenroll(c): + return error_response(400, f"User {uid} is not enrolled in course {cid}") + + resp = {"user": u.to_dict(), "course": c.to_dict()} + return jsonify(resp) + diff --git a/backend/migrations/versions/862905f5e34a_add_course_code.py b/backend/migrations/versions/862905f5e34a_add_course_code.py new file mode 100644 index 0000000..d408a8b --- /dev/null +++ b/backend/migrations/versions/862905f5e34a_add_course_code.py @@ -0,0 +1,34 @@ +"""Add course code + +Revision ID: 862905f5e34a +Revises: 093a66f0b581 +Create Date: 2023-04-06 21:55:54.838647 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '862905f5e34a' +down_revision = '093a66f0b581' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('course', schema=None) as batch_op: + batch_op.add_column(sa.Column('course_code', sa.String(length=32), nullable=True)) + batch_op.create_index(batch_op.f('ix_course_course_code'), ['course_code'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('course', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_course_course_code')) + batch_op.drop_column('course_code') + + # ### end Alembic commands ### diff --git a/backend/requirements.txt b/backend/requirements.txt index 6937fee..d11b006 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,6 +12,7 @@ itsdangerous==2.1.2 Jinja2==3.1.2 Mako==1.2.4 MarkupSafe==2.1.2 +mysqlclient==2.1.1 python-dotenv==1.0.0 six==1.16.0 SQLAlchemy==2.0.6 diff --git a/frontend/public/index.html b/frontend/public/index.html index 104b21e..29efbd7 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -22,7 +22,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + LearningTree