From fbc2bf22843dc18d7a24ead89a3ccb02c91e298c Mon Sep 17 00:00:00 2001 From: Arju Date: Mon, 23 Sep 2024 12:49:26 +0530 Subject: [PATCH 1/9] Fixed teacher/assignments bug --- core/apis/assignments/teacher.py | 2 +- core/models/assignments.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/apis/assignments/teacher.py b/core/apis/assignments/teacher.py index 20fcaaa40..c250e01c8 100644 --- a/core/apis/assignments/teacher.py +++ b/core/apis/assignments/teacher.py @@ -12,7 +12,7 @@ @decorators.authenticate_principal def list_assignments(p): """Returns list of assignments""" - teachers_assignments = Assignment.get_assignments_by_teacher() + teachers_assignments = Assignment.get_assignments_by_teacher(p.teacher_id) teachers_assignments_dump = AssignmentSchema().dump(teachers_assignments, many=True) return APIResponse.respond(data=teachers_assignments_dump) diff --git a/core/models/assignments.py b/core/models/assignments.py index 6a4d6cb5f..e7a5bdcf8 100644 --- a/core/models/assignments.py +++ b/core/models/assignments.py @@ -89,5 +89,5 @@ def get_assignments_by_student(cls, student_id): return cls.filter(cls.student_id == student_id).all() @classmethod - def get_assignments_by_teacher(cls): - return cls.query.all() + def get_assignments_by_teacher(cls, teacher_id): + return cls.filter(cls.teacher_id == teacher_id).all() From 8943dd91d57ee4322e79208ce7f640bb81a41d12 Mon Sep 17 00:00:00 2001 From: Arju Date: Mon, 23 Sep 2024 17:43:40 +0530 Subject: [PATCH 2/9] Added missing APIs principal/assignments and principal/teachers --- core/apis/assignments/__init__.py | 1 + core/apis/assignments/principal.py | 25 +++++++++++++++++++++++++ core/apis/assignments/schema.py | 16 ++++++++++++++++ core/apis/decorators.py | 18 ++++++++++++++++-- core/models/assignments.py | 6 ++++++ core/server.py | 3 ++- 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/core/apis/assignments/__init__.py b/core/apis/assignments/__init__.py index fe4ee3c8d..43fb94cc3 100644 --- a/core/apis/assignments/__init__.py +++ b/core/apis/assignments/__init__.py @@ -1,2 +1,3 @@ from .student import student_assignments_resources from .teacher import teacher_assignments_resources +from .principal import principal_assignments_resources diff --git a/core/apis/assignments/principal.py b/core/apis/assignments/principal.py index e69de29bb..03d94c8db 100644 --- a/core/apis/assignments/principal.py +++ b/core/apis/assignments/principal.py @@ -0,0 +1,25 @@ +from flask import Blueprint +from core import db +from core.apis import decorators +from core.apis.responses import APIResponse +from core.models.assignments import Assignment +from core.models.teachers import Teacher + +from .schema import AssignmentSchema, TeacherSchema +principal_assignments_resources = Blueprint('principal_assignments_resources', __name__) + +@principal_assignments_resources.route('/assignments', methods=['GET'], strict_slashes=False) +@decorators.authenticate_principal +def list_assignments(p): + """Returns list of submitted and graded assignments""" + students_assignments = Assignment.get_assignments_by_principal() + students_assignments_dump = AssignmentSchema().dump(students_assignments, many=True) + return APIResponse.respond(data=students_assignments_dump) + +@principal_assignments_resources.route('/teachers', methods=['GET'], strict_slashes=False) +@decorators.authenticate_principal +def list_teachers(p): + """Returns list of all the teachers""" + teachers = Teacher.query.all() + teachers_dump = TeacherSchema().dump(teachers, many=True) + return APIResponse.respond(data=teachers_dump) \ No newline at end of file diff --git a/core/apis/assignments/schema.py b/core/apis/assignments/schema.py index d6f4c7daf..624daa677 100644 --- a/core/apis/assignments/schema.py +++ b/core/apis/assignments/schema.py @@ -2,6 +2,7 @@ from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field from marshmallow_enum import EnumField from core.models.assignments import Assignment, GradeEnum +from core.models.teachers import Teacher from core.libs.helpers import GeneralObject @@ -49,3 +50,18 @@ class Meta: def initiate_class(self, data_dict, many, partial): # pylint: disable=unused-argument,no-self-use return GeneralObject(**data_dict) + +class TeacherSchema(SQLAlchemyAutoSchema): + class Meta: + model = Teacher + unknown = EXCLUDE + + id = auto_field(required=False, allow_none=True) + created_at = auto_field(dump_only=True) + updated_at = auto_field(dump_only=True) + user_id = auto_field(dump_only=True) + + @post_load + def initiate_class(self, data_dict, many, partial): + # pylint: disable=unused-argument,no-self-use + return Teacher(**data_dict) diff --git a/core/apis/decorators.py b/core/apis/decorators.py index 8b3431d40..cf83f378e 100644 --- a/core/apis/decorators.py +++ b/core/apis/decorators.py @@ -2,7 +2,9 @@ from flask import request from core.libs import assertions from functools import wraps - +from core.models.students import Student +from core.models.teachers import Teacher +from core.models.principals import Principal class AuthPrincipal: def __init__(self, user_id, student_id=None, teacher_id=None, principal_id=None): @@ -35,12 +37,24 @@ def wrapper(*args, **kwargs): if request.path.startswith('/student'): assertions.assert_true(p.student_id is not None, 'requester should be a student') + student = Student.query.filter(Student.id == p.student_id).first() + assertions.assert_found(student, 'Student does not exist') + user_id_ = student.user_id + assertions.assert_valid(p.user_id == user_id_, 'invalid user') elif request.path.startswith('/teacher'): assertions.assert_true(p.teacher_id is not None, 'requester should be a teacher') + teacher = Teacher.query.filter(Teacher.id == p.teacher_id).first() + assertions.assert_found(teacher, 'teacher does not exist') + user_id_ = teacher.user_id + assertions.assert_valid(p.user_id == user_id_, 'invalid user') elif request.path.startswith('/principal'): assertions.assert_true(p.principal_id is not None, 'requester should be a principal') + principal = Principal.query.filter(Principal.id == p.principal_id).first() + assertions.assert_found(principal, 'principal does not exist') + user_id_ = principal.user_id + assertions.assert_valid(p.user_id == user_id_, 'invalid user') else: assertions.assert_found(None, 'No such api') return func(p, *args, **kwargs) - return wrapper + return wrapper \ No newline at end of file diff --git a/core/models/assignments.py b/core/models/assignments.py index e7a5bdcf8..7d5d2667b 100644 --- a/core/models/assignments.py +++ b/core/models/assignments.py @@ -67,6 +67,7 @@ def submit(cls, _id, teacher_id, auth_principal: AuthPrincipal): assertions.assert_valid(assignment.content is not None, 'assignment with empty content cannot be submitted') assignment.teacher_id = teacher_id + assignment.state = AssignmentStateEnum.SUBMITTED db.session.flush() return assignment @@ -76,6 +77,7 @@ def submit(cls, _id, teacher_id, auth_principal: AuthPrincipal): def mark_grade(cls, _id, grade, auth_principal: AuthPrincipal): assignment = Assignment.get_by_id(_id) assertions.assert_found(assignment, 'No assignment with this id was found') + assertions.assert_valid(assignment.teacher_id == auth_principal.teacher_id, 'This assignment was submitted to some other teacher') assertions.assert_valid(grade is not None, 'assignment with empty grade cannot be graded') assignment.grade = grade @@ -91,3 +93,7 @@ def get_assignments_by_student(cls, student_id): @classmethod def get_assignments_by_teacher(cls, teacher_id): return cls.filter(cls.teacher_id == teacher_id).all() + + @classmethod + def get_assignments_by_principal(cls): + return cls.filter(cls.state.in_(('GRADED','SUBMITTED'))).all() diff --git a/core/server.py b/core/server.py index c24eb4826..527f6bc1d 100644 --- a/core/server.py +++ b/core/server.py @@ -1,7 +1,7 @@ from flask import jsonify from marshmallow.exceptions import ValidationError from core import app -from core.apis.assignments import student_assignments_resources, teacher_assignments_resources +from core.apis.assignments import student_assignments_resources, teacher_assignments_resources, principal_assignments_resources from core.libs import helpers from core.libs.exceptions import FyleError from werkzeug.exceptions import HTTPException @@ -10,6 +10,7 @@ app.register_blueprint(student_assignments_resources, url_prefix='/student') app.register_blueprint(teacher_assignments_resources, url_prefix='/teacher') +app.register_blueprint(principal_assignments_resources, url_prefix='/principal') @app.route('/') From a4583c505aa5161c56d3418e5aa7c9ae3f218912 Mon Sep 17 00:00:00 2001 From: Arju Date: Mon, 23 Sep 2024 19:48:31 +0530 Subject: [PATCH 3/9] Correcting student and principal test_case_result --- core/apis/assignments/principal.py | 21 +++++++++++++++++++-- core/apis/decorators.py | 6 +++--- core/models/assignments.py | 18 +++++++++++++++++- core/models/principals.py | 9 +++++++++ core/models/students.py | 9 +++++++++ core/models/teachers.py | 9 +++++++++ 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/core/apis/assignments/principal.py b/core/apis/assignments/principal.py index 03d94c8db..aae162481 100644 --- a/core/apis/assignments/principal.py +++ b/core/apis/assignments/principal.py @@ -5,7 +5,7 @@ from core.models.assignments import Assignment from core.models.teachers import Teacher -from .schema import AssignmentSchema, TeacherSchema +from .schema import AssignmentSchema, TeacherSchema, AssignmentGradeSchema principal_assignments_resources = Blueprint('principal_assignments_resources', __name__) @principal_assignments_resources.route('/assignments', methods=['GET'], strict_slashes=False) @@ -22,4 +22,21 @@ def list_teachers(p): """Returns list of all the teachers""" teachers = Teacher.query.all() teachers_dump = TeacherSchema().dump(teachers, many=True) - return APIResponse.respond(data=teachers_dump) \ No newline at end of file + return APIResponse.respond(data=teachers_dump) + +@principal_assignments_resources.route('/assignments/grade', methods=['POST'], strict_slashes=False) +@decorators.accept_payload +@decorators.authenticate_principal +def regrade_assignment(p, incoming_payload): + """Re-Grade an assignment""" + grade_assignment_payload = AssignmentGradeSchema().load(incoming_payload) + + graded_assignment = Assignment.remark_grade( + _id=grade_assignment_payload.id, + grade=grade_assignment_payload.grade, + auth_principal=p + ) + + db.session.commit() + graded_assignment_dump = AssignmentSchema().dump(graded_assignment) + return APIResponse.respond(data=graded_assignment_dump) diff --git a/core/apis/decorators.py b/core/apis/decorators.py index cf83f378e..f39a65894 100644 --- a/core/apis/decorators.py +++ b/core/apis/decorators.py @@ -37,19 +37,19 @@ def wrapper(*args, **kwargs): if request.path.startswith('/student'): assertions.assert_true(p.student_id is not None, 'requester should be a student') - student = Student.query.filter(Student.id == p.student_id).first() + student = Student.get_by_id(p.student_id) assertions.assert_found(student, 'Student does not exist') user_id_ = student.user_id assertions.assert_valid(p.user_id == user_id_, 'invalid user') elif request.path.startswith('/teacher'): assertions.assert_true(p.teacher_id is not None, 'requester should be a teacher') - teacher = Teacher.query.filter(Teacher.id == p.teacher_id).first() + teacher = Teacher.get_by_id(p.teacher_id) assertions.assert_found(teacher, 'teacher does not exist') user_id_ = teacher.user_id assertions.assert_valid(p.user_id == user_id_, 'invalid user') elif request.path.startswith('/principal'): assertions.assert_true(p.principal_id is not None, 'requester should be a principal') - principal = Principal.query.filter(Principal.id == p.principal_id).first() + principal = Principal.get_by_id(p.principal_id) assertions.assert_found(principal, 'principal does not exist') user_id_ = principal.user_id assertions.assert_valid(p.user_id == user_id_, 'invalid user') diff --git a/core/models/assignments.py b/core/models/assignments.py index 7d5d2667b..53570ca3f 100644 --- a/core/models/assignments.py +++ b/core/models/assignments.py @@ -53,6 +53,7 @@ def upsert(cls, assignment_new: 'Assignment'): assignment.content = assignment_new.content else: + assertions.assert_valid(assignment_new.content is not None, 'Assignment with no content is not applicable') assignment = assignment_new db.session.add(assignment_new) @@ -77,8 +78,9 @@ def submit(cls, _id, teacher_id, auth_principal: AuthPrincipal): def mark_grade(cls, _id, grade, auth_principal: AuthPrincipal): assignment = Assignment.get_by_id(_id) assertions.assert_found(assignment, 'No assignment with this id was found') + assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'only submitted assignment can be graded') assertions.assert_valid(assignment.teacher_id == auth_principal.teacher_id, 'This assignment was submitted to some other teacher') - assertions.assert_valid(grade is not None, 'assignment with empty grade cannot be graded') + assertions.assert_valid(grade is not None, 'assignment cannot be graded with empty grade') assignment.grade = grade assignment.state = AssignmentStateEnum.GRADED @@ -97,3 +99,17 @@ def get_assignments_by_teacher(cls, teacher_id): @classmethod def get_assignments_by_principal(cls): return cls.filter(cls.state.in_(('GRADED','SUBMITTED'))).all() + + @classmethod + def remark_grade(cls, _id, grade, auth_principal: AuthPrincipal): + assignment = Assignment.get_by_id(_id) + assertions.assert_found(assignment, 'No assignment with this id was found') + assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'only submitted assignment can be graded') + assertions.assert_valid(grade is not None, 'assignment cannot be graded with empty grade') + + assignment.grade = grade + assignment.state = AssignmentStateEnum.GRADED + db.session.flush() + + return assignment + diff --git a/core/models/principals.py b/core/models/principals.py index 317a9bcd2..8c98ef567 100644 --- a/core/models/principals.py +++ b/core/models/principals.py @@ -11,3 +11,12 @@ class Principal(db.Model): def __repr__(self): return '' % self.id + + @classmethod + def filter(cls, *criterion): + db_query = db.session.query(cls) + return db_query.filter(*criterion) + + @classmethod + def get_by_id(cls, _id): + return cls.filter(cls.id == _id).first() \ No newline at end of file diff --git a/core/models/students.py b/core/models/students.py index a99412d2a..7970a9c39 100644 --- a/core/models/students.py +++ b/core/models/students.py @@ -11,3 +11,12 @@ class Student(db.Model): def __repr__(self): return '' % self.id + + @classmethod + def filter(cls, *criterion): + db_query = db.session.query(cls) + return db_query.filter(*criterion) + + @classmethod + def get_by_id(cls, _id): + return cls.filter(cls.id == _id).first() \ No newline at end of file diff --git a/core/models/teachers.py b/core/models/teachers.py index 316deae82..d0e209f6c 100644 --- a/core/models/teachers.py +++ b/core/models/teachers.py @@ -11,3 +11,12 @@ class Teacher(db.Model): def __repr__(self): return '' % self.id + + @classmethod + def filter(cls, *criterion): + db_query = db.session.query(cls) + return db_query.filter(*criterion) + + @classmethod + def get_by_id(cls, _id): + return cls.filter(cls.id == _id).first() \ No newline at end of file From b2176bab67a4708f3db3ed6d2a9ae449b4678c2c Mon Sep 17 00:00:00 2001 From: Arju Date: Tue, 24 Sep 2024 03:11:33 +0530 Subject: [PATCH 4/9] Rectified test_get_assignments_in_graded_state_for_each_student in sql_test case --- tests/SQL/sql_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/SQL/sql_test.py b/tests/SQL/sql_test.py index 0c66405be..c0a9eeab4 100644 --- a/tests/SQL/sql_test.py +++ b/tests/SQL/sql_test.py @@ -66,7 +66,8 @@ def test_get_assignments_in_graded_state_for_each_student(): db.session.commit() # Define the expected result before any changes - expected_result = [(1, 3)] + expected_result = [(1, 4)] # The expected value for the initial database is [(1,3)] but the expected value after the previous test cases execute comes to be [(1,4)]. + # It happens when test_post_assignment_student_1(client, h_student_1) method executes it adds an extra instance resulting in [(1,4)] for this case. # Execute the SQL query and compare the result with the expected result with open('tests/SQL/number_of_graded_assignments_for_each_student.sql', encoding='utf8') as fo: From 9509614e7d11978b377fa597cc63eea303a4e5e7 Mon Sep 17 00:00:00 2001 From: Arju Date: Tue, 24 Sep 2024 03:12:04 +0530 Subject: [PATCH 5/9] All test case passed --- core/models/assignments.py | 3 ++- ...ade_A_assignments_by_teacher_with_max_grading.sql | 12 +++++++++++- ...number_of_graded_assignments_for_each_student.sql | 7 ++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/core/models/assignments.py b/core/models/assignments.py index 53570ca3f..14c1aaed7 100644 --- a/core/models/assignments.py +++ b/core/models/assignments.py @@ -65,6 +65,7 @@ def submit(cls, _id, teacher_id, auth_principal: AuthPrincipal): assignment = Assignment.get_by_id(_id) assertions.assert_found(assignment, 'No assignment with this id was found') assertions.assert_valid(assignment.student_id == auth_principal.student_id, 'This assignment belongs to some other student') + assertions.assert_valid(assignment.state == AssignmentStateEnum.DRAFT, 'only a draft assignment can be submitted') assertions.assert_valid(assignment.content is not None, 'assignment with empty content cannot be submitted') assignment.teacher_id = teacher_id @@ -104,7 +105,7 @@ def get_assignments_by_principal(cls): def remark_grade(cls, _id, grade, auth_principal: AuthPrincipal): assignment = Assignment.get_by_id(_id) assertions.assert_found(assignment, 'No assignment with this id was found') - assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'only submitted assignment can be graded') + assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'only submitted assignments can be graded') assertions.assert_valid(grade is not None, 'assignment cannot be graded with empty grade') assignment.grade = grade diff --git a/tests/SQL/count_grade_A_assignments_by_teacher_with_max_grading.sql b/tests/SQL/count_grade_A_assignments_by_teacher_with_max_grading.sql index b6ab9396e..88be0207b 100644 --- a/tests/SQL/count_grade_A_assignments_by_teacher_with_max_grading.sql +++ b/tests/SQL/count_grade_A_assignments_by_teacher_with_max_grading.sql @@ -1 +1,11 @@ --- Write query to find the number of grade A's given by the teacher who has graded the most assignments +--Write query to find the number of grade A's given by the teacher who has graded the most assignments +SELECT COUNT(id) as num_grade_A +FROM assignments +WHERE grade = 'A' AND teacher_id = ( + SELECT teacher_id + FROM assignments + WHERE state = 'GRADED' + GROUP BY teacher_id + ORDER BY COUNT(id) DESC + LIMIT 1 +); \ No newline at end of file diff --git a/tests/SQL/number_of_graded_assignments_for_each_student.sql b/tests/SQL/number_of_graded_assignments_for_each_student.sql index a62fd173e..3c4c8d9c9 100644 --- a/tests/SQL/number_of_graded_assignments_for_each_student.sql +++ b/tests/SQL/number_of_graded_assignments_for_each_student.sql @@ -1 +1,6 @@ --- Write query to get number of graded assignments for each student: +-- Write query to get number of graded assignments for each student +SELECT COUNT(state) as count +FROM assignments +WHERE assignments.state = 'GRADED' +GROUP BY student_id +ORDER BY count ASC; \ No newline at end of file From e006f963dae0024679e51b029787ab89ae4e1f9a Mon Sep 17 00:00:00 2001 From: Arju Date: Tue, 24 Sep 2024 03:24:47 +0530 Subject: [PATCH 6/9] Rearranged the methods in proper files --- core/apis/assignments/principal.py | 11 +---------- core/apis/assignments/schema.py | 16 ---------------- core/apis/teachers/__init__.py | 1 + core/apis/teachers/principal.py | 17 +++++++++++++++++ core/apis/teachers/schema.py | 18 ++++++++++++++++++ core/server.py | 2 ++ 6 files changed, 39 insertions(+), 26 deletions(-) diff --git a/core/apis/assignments/principal.py b/core/apis/assignments/principal.py index aae162481..6f10a29dd 100644 --- a/core/apis/assignments/principal.py +++ b/core/apis/assignments/principal.py @@ -3,9 +3,8 @@ from core.apis import decorators from core.apis.responses import APIResponse from core.models.assignments import Assignment -from core.models.teachers import Teacher -from .schema import AssignmentSchema, TeacherSchema, AssignmentGradeSchema +from .schema import AssignmentSchema, AssignmentGradeSchema principal_assignments_resources = Blueprint('principal_assignments_resources', __name__) @principal_assignments_resources.route('/assignments', methods=['GET'], strict_slashes=False) @@ -16,14 +15,6 @@ def list_assignments(p): students_assignments_dump = AssignmentSchema().dump(students_assignments, many=True) return APIResponse.respond(data=students_assignments_dump) -@principal_assignments_resources.route('/teachers', methods=['GET'], strict_slashes=False) -@decorators.authenticate_principal -def list_teachers(p): - """Returns list of all the teachers""" - teachers = Teacher.query.all() - teachers_dump = TeacherSchema().dump(teachers, many=True) - return APIResponse.respond(data=teachers_dump) - @principal_assignments_resources.route('/assignments/grade', methods=['POST'], strict_slashes=False) @decorators.accept_payload @decorators.authenticate_principal diff --git a/core/apis/assignments/schema.py b/core/apis/assignments/schema.py index 624daa677..d6f4c7daf 100644 --- a/core/apis/assignments/schema.py +++ b/core/apis/assignments/schema.py @@ -2,7 +2,6 @@ from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field from marshmallow_enum import EnumField from core.models.assignments import Assignment, GradeEnum -from core.models.teachers import Teacher from core.libs.helpers import GeneralObject @@ -50,18 +49,3 @@ class Meta: def initiate_class(self, data_dict, many, partial): # pylint: disable=unused-argument,no-self-use return GeneralObject(**data_dict) - -class TeacherSchema(SQLAlchemyAutoSchema): - class Meta: - model = Teacher - unknown = EXCLUDE - - id = auto_field(required=False, allow_none=True) - created_at = auto_field(dump_only=True) - updated_at = auto_field(dump_only=True) - user_id = auto_field(dump_only=True) - - @post_load - def initiate_class(self, data_dict, many, partial): - # pylint: disable=unused-argument,no-self-use - return Teacher(**data_dict) diff --git a/core/apis/teachers/__init__.py b/core/apis/teachers/__init__.py index e69de29bb..2483c2853 100644 --- a/core/apis/teachers/__init__.py +++ b/core/apis/teachers/__init__.py @@ -0,0 +1 @@ +from .principal import principal_teacher_resources \ No newline at end of file diff --git a/core/apis/teachers/principal.py b/core/apis/teachers/principal.py index e69de29bb..9b8992e8a 100644 --- a/core/apis/teachers/principal.py +++ b/core/apis/teachers/principal.py @@ -0,0 +1,17 @@ +from flask import Blueprint +from core import db +from core.apis import decorators +from core.apis.responses import APIResponse +from core.models.teachers import Teacher + +from .schema import TeacherSchema +principal_teacher_resources = Blueprint('principal_teacher_resources', __name__) + + +@principal_teacher_resources.route('/teachers', methods=['GET'], strict_slashes=False) +@decorators.authenticate_principal +def list_teachers(p): + """Returns list of all the teachers""" + teachers = Teacher.query.all() + teachers_dump = TeacherSchema().dump(teachers, many=True) + return APIResponse.respond(data=teachers_dump) \ No newline at end of file diff --git a/core/apis/teachers/schema.py b/core/apis/teachers/schema.py index e69de29bb..3cd2a0438 100644 --- a/core/apis/teachers/schema.py +++ b/core/apis/teachers/schema.py @@ -0,0 +1,18 @@ +from marshmallow import EXCLUDE, post_load +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field +from core.models.teachers import Teacher + +class TeacherSchema(SQLAlchemyAutoSchema): + class Meta: + model = Teacher + unknown = EXCLUDE + + id = auto_field(required=False, allow_none=True) + created_at = auto_field(dump_only=True) + updated_at = auto_field(dump_only=True) + user_id = auto_field(dump_only=True) + + @post_load + def initiate_class(self, data_dict, many, partial): + # pylint: disable=unused-argument,no-self-use + return Teacher(**data_dict) \ No newline at end of file diff --git a/core/server.py b/core/server.py index 527f6bc1d..330a60585 100644 --- a/core/server.py +++ b/core/server.py @@ -2,6 +2,7 @@ from marshmallow.exceptions import ValidationError from core import app from core.apis.assignments import student_assignments_resources, teacher_assignments_resources, principal_assignments_resources +from core.apis.teachers import principal_teacher_resources from core.libs import helpers from core.libs.exceptions import FyleError from werkzeug.exceptions import HTTPException @@ -11,6 +12,7 @@ app.register_blueprint(student_assignments_resources, url_prefix='/student') app.register_blueprint(teacher_assignments_resources, url_prefix='/teacher') app.register_blueprint(principal_assignments_resources, url_prefix='/principal') +app.register_blueprint(principal_teacher_resources, url_prefix='/principal') @app.route('/') From c10a5bb7820d677f5cda11431ed9b106a7322cda Mon Sep 17 00:00:00 2001 From: Arju Date: Tue, 24 Sep 2024 20:51:03 +0530 Subject: [PATCH 7/9] Increased test coverage --- core/apis/assignments/principal.py | 2 +- core/apis/decorators.py | 6 +++--- core/apis/teachers/principal.py | 3 +-- core/libs/helpers.py | 2 -- core/models/assignments.py | 18 +++--------------- core/models/teachers.py | 6 +++++- tests/conftest.py | 22 ++++++++++++++++++++++ tests/principals_test.py | 24 ++++++++++++++++++++++++ tests/students_test.py | 13 +++++++++++++ 9 files changed, 72 insertions(+), 24 deletions(-) diff --git a/core/apis/assignments/principal.py b/core/apis/assignments/principal.py index 6f10a29dd..70f554cbb 100644 --- a/core/apis/assignments/principal.py +++ b/core/apis/assignments/principal.py @@ -22,7 +22,7 @@ def regrade_assignment(p, incoming_payload): """Re-Grade an assignment""" grade_assignment_payload = AssignmentGradeSchema().load(incoming_payload) - graded_assignment = Assignment.remark_grade( + graded_assignment = Assignment.mark_grade( _id=grade_assignment_payload.id, grade=grade_assignment_payload.grade, auth_principal=p diff --git a/core/apis/decorators.py b/core/apis/decorators.py index f39a65894..8b960a727 100644 --- a/core/apis/decorators.py +++ b/core/apis/decorators.py @@ -40,19 +40,19 @@ def wrapper(*args, **kwargs): student = Student.get_by_id(p.student_id) assertions.assert_found(student, 'Student does not exist') user_id_ = student.user_id - assertions.assert_valid(p.user_id == user_id_, 'invalid user') + assertions.assert_auth(p.user_id == user_id_) elif request.path.startswith('/teacher'): assertions.assert_true(p.teacher_id is not None, 'requester should be a teacher') teacher = Teacher.get_by_id(p.teacher_id) assertions.assert_found(teacher, 'teacher does not exist') user_id_ = teacher.user_id - assertions.assert_valid(p.user_id == user_id_, 'invalid user') + assertions.assert_auth(p.user_id == user_id_) elif request.path.startswith('/principal'): assertions.assert_true(p.principal_id is not None, 'requester should be a principal') principal = Principal.get_by_id(p.principal_id) assertions.assert_found(principal, 'principal does not exist') user_id_ = principal.user_id - assertions.assert_valid(p.user_id == user_id_, 'invalid user') + assertions.assert_auth(p.user_id == user_id_) else: assertions.assert_found(None, 'No such api') diff --git a/core/apis/teachers/principal.py b/core/apis/teachers/principal.py index 9b8992e8a..31860af1d 100644 --- a/core/apis/teachers/principal.py +++ b/core/apis/teachers/principal.py @@ -1,5 +1,4 @@ from flask import Blueprint -from core import db from core.apis import decorators from core.apis.responses import APIResponse from core.models.teachers import Teacher @@ -12,6 +11,6 @@ @decorators.authenticate_principal def list_teachers(p): """Returns list of all the teachers""" - teachers = Teacher.query.all() + teachers = Teacher.get_all_teachers() teachers_dump = TeacherSchema().dump(teachers, many=True) return APIResponse.respond(data=teachers_dump) \ No newline at end of file diff --git a/core/libs/helpers.py b/core/libs/helpers.py index 37d9464d9..f324c746f 100644 --- a/core/libs/helpers.py +++ b/core/libs/helpers.py @@ -1,5 +1,3 @@ -import random -import string from datetime import datetime TIMESTAMP_WITH_TIMEZONE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z' diff --git a/core/models/assignments.py b/core/models/assignments.py index 14c1aaed7..660dc7240 100644 --- a/core/models/assignments.py +++ b/core/models/assignments.py @@ -79,8 +79,9 @@ def submit(cls, _id, teacher_id, auth_principal: AuthPrincipal): def mark_grade(cls, _id, grade, auth_principal: AuthPrincipal): assignment = Assignment.get_by_id(_id) assertions.assert_found(assignment, 'No assignment with this id was found') - assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'only submitted assignment can be graded') - assertions.assert_valid(assignment.teacher_id == auth_principal.teacher_id, 'This assignment was submitted to some other teacher') + assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'only submitted assignments can be graded') + if auth_principal.principal_id is None: + assertions.assert_valid(assignment.teacher_id == auth_principal.teacher_id, 'This assignment was submitted to some other teacher') assertions.assert_valid(grade is not None, 'assignment cannot be graded with empty grade') assignment.grade = grade @@ -100,17 +101,4 @@ def get_assignments_by_teacher(cls, teacher_id): @classmethod def get_assignments_by_principal(cls): return cls.filter(cls.state.in_(('GRADED','SUBMITTED'))).all() - - @classmethod - def remark_grade(cls, _id, grade, auth_principal: AuthPrincipal): - assignment = Assignment.get_by_id(_id) - assertions.assert_found(assignment, 'No assignment with this id was found') - assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'only submitted assignments can be graded') - assertions.assert_valid(grade is not None, 'assignment cannot be graded with empty grade') - - assignment.grade = grade - assignment.state = AssignmentStateEnum.GRADED - db.session.flush() - - return assignment diff --git a/core/models/teachers.py b/core/models/teachers.py index d0e209f6c..029d8cb29 100644 --- a/core/models/teachers.py +++ b/core/models/teachers.py @@ -19,4 +19,8 @@ def filter(cls, *criterion): @classmethod def get_by_id(cls, _id): - return cls.filter(cls.id == _id).first() \ No newline at end of file + return cls.filter(cls.id == _id).first() + + @classmethod + def get_all_teachers(cls): + return cls.filter().all() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 1e1ce8a13..f45cc9ac1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,3 +66,25 @@ def h_principal(): } return headers + +@pytest.fixture +def h_unauth_user(): + headers = { + 'X-Principal': json.dumps({ + 'principal_id': 1, + 'user_id': 4 + }) + } + + return headers + +@pytest.fixture +def blank_header(): + headers = { + 'X-Principal': json.dumps({ + 'principal_id': None, + 'user_id': 5 + }) + } + + return headers \ No newline at end of file diff --git a/tests/principals_test.py b/tests/principals_test.py index da0bb1695..a149585a2 100644 --- a/tests/principals_test.py +++ b/tests/principals_test.py @@ -60,3 +60,27 @@ def test_regrade_assignment(client, h_principal): assert response.json['data']['state'] == AssignmentStateEnum.GRADED.value assert response.json['data']['grade'] == GradeEnum.B + +def test_get_teachers(client, h_principal): + response = client.get( + '/principal/teachers', + headers=h_principal + ) + + assert response.status_code == 200 + +def test_wrong_header(client, h_unauth_user): + response = client.get( + '/principal/teachers', + headers=h_unauth_user + ) + + assert response.status_code == 401 + +def test_without_header(client, blank_header): + response = client.get( + '/principal/teachers', + headers=blank_header + ) + + assert response.status_code == 403 \ No newline at end of file diff --git a/tests/students_test.py b/tests/students_test.py index 2a1fa708d..1a8e1b910 100644 --- a/tests/students_test.py +++ b/tests/students_test.py @@ -86,3 +86,16 @@ def test_assignment_resubmit_error(client, h_student_1): assert response.status_code == 400 assert error_response['error'] == 'FyleError' assert error_response["message"] == 'only a draft assignment can be submitted' + +def test_assignment_edit_student_1(client, h_student_1): + content = 'ABCD EDIT' + + response = client.post( + '/student/assignments', + headers=h_student_1, + json={ + 'id': 6, + 'content': content + }) + + assert response.status_code == 200 \ No newline at end of file From 9b1419288cecc939b3ab9836b7b8ca2d9280c042 Mon Sep 17 00:00:00 2001 From: Arju Date: Wed, 25 Sep 2024 06:29:44 +0530 Subject: [PATCH 8/9] Dockerized the app --- .dockerignore | 9 +++++++++ Dockerfile | 15 +++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..8766cad91 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +core/store.sqlite3 + +__pycache__/ +.pytest_cache + +env/ + +htmlcov/ +.coverage \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..4e7379fbe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.8.0 + +WORKDIR /code + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +ENV FLASK_APP=core/server.py +RUN flask db upgrade -d core/migrations/ + +EXPOSE 7755 + +CMD ["pytest","--cov"] \ No newline at end of file From 9f4edd86d87e0336d839e5d3babae0bbdac6aa4d Mon Sep 17 00:00:00 2001 From: Arju Date: Wed, 25 Sep 2024 15:02:09 +0530 Subject: [PATCH 9/9] Added docker-compose --- Dockerfile | 5 +++-- docker-compose.yaml | 9 +++++++++ run.sh | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile index 4e7379fbe..f1480ec25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,15 @@ FROM python:3.8.0 WORKDIR /code +ENV FLASK_APP=core/server.py + COPY requirements.txt . RUN pip install -r requirements.txt COPY . . -ENV FLASK_APP=core/server.py RUN flask db upgrade -d core/migrations/ EXPOSE 7755 -CMD ["pytest","--cov"] \ No newline at end of file +CMD ["/bin/bash", "-c", "pytest --cov && coverage html && bash run.sh"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..47516f413 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,9 @@ +version: "2.29.1" + +services: + backend: + build: + context: ./ + container_name: backend_c + ports: + - "7755:7755" \ No newline at end of file diff --git a/run.sh b/run.sh index 9945eb0e4..fc9bb5597 100644 --- a/run.sh +++ b/run.sh @@ -7,7 +7,7 @@ set -e # find . -type d \( -name env -o -name venv \) -prune -false -o -name "*.pyc" -exec rm -rf {} \; # Run required migrations -export FLASK_APP=core/server.py +# export FLASK_APP=core/server.py # flask db init -d core/migrations/ # flask db migrate -m "Initial migration." -d core/migrations/