Skip to content

Commit

Permalink
feat: New code challenge and add SQL tests (fylein#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
KirtiGautam authored Jan 9, 2024
1 parent e97b4d1 commit d066d7d
Show file tree
Hide file tree
Showing 22 changed files with 435 additions and 28 deletions.
95 changes: 88 additions & 7 deletions Application.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

There are 4 resources:
- Users
- Principal
- Students
- Teachers
- Assignments

4 Users (2 students and 2 teachers) have already been created for you in the db fixture
5 Users (1 Principal, 2 students and 2 teachers) have already been created for you in the db fixture

- A principal can view all the teachers
- A principal can view all the assignments submitted and/or graded by teachers.
- A principal can re-grade the assignments already graded by the teacher.
- A student can create and edit a draft assignment
- A student can list all his created assignments
- A student can submit a draft assignment to a teacher
Expand All @@ -18,9 +22,13 @@ There are 4 resources:

Your tasks
- Add missing APIs mentioned here and get the automated tests to pass
- Add a test for grading API
- Add tests for grading API
- All tests should pass
- Get the test coverage to 94% or above
- There are certain SQL tests present inside `tests/SQL/`. You have to write SQL in following files:
- count_grade_A_assignments_by_teacher_with_max_grading.sql
- number_of_assignments_per_state.sql
- Optionally, Dockerize your application by creating a Dockerfile and a docker-compose.yml file, providing clear documentation on building and running the application with Docker, to stand out in your submission

Once you are done with your task, please use [this form](https://forms.gle/nWVJe1kLPgNmpVoM8) to complete your submission.

Expand Down Expand Up @@ -150,12 +158,8 @@ response:
"updated_at": "2021-09-17T03:17:20.147349"
}
}
```

## Missing APIs

You'll need to implement these APIs
```
### GET /teacher/assignments

List all assignments submitted to this teacher
Expand Down Expand Up @@ -207,3 +211,80 @@ response:
}
}
```

## Missing APIs

You'll need to implement these APIs

### GET /principal/assignments

List all assignments submitted to this teacher
```
headers:
X-Principal: {"user_id":5, "principal_id":1}
response:
{
"data": [
{
"content": "ESSAY T1",
"created_at": "2021-09-17T03:14:01.580126",
"grade": null,
"id": 1,
"state": "SUBMITTED",
"student_id": 1,
"teacher_id": 1,
"updated_at": "2021-09-17T03:14:01.584644"
}
]
}
```

### GET /principal/teachers

Get all the teachers
```
headers:
X-Principal: {"user_id":5, "principal_id":1}
response:
{
"data": [
{
"created_at": "2024-01-08T07:58:53.131970",
"id": 1,
"updated_at": "2024-01-08T07:58:53.131972",
"user_id": 3
}
]
}
```

### POST /principal/assignments/grade

Grade or re-grade an assignment
```
headers:
X-Principal: {"user_id":5, "principal_id":1}
payload:
{
"id": 1,
"grade": "A"
}
response:
{
"data": {
"content": "ESSAY T1",
"created_at": "2021-09-17T03:14:01.580126",
"grade": "A",
"id": 1,
"state": "GRADED",
"student_id": 1,
"teacher_id": 1,
"updated_at": "2021-09-17T03:20:42.896947"
}
}
```
1 change: 1 addition & 0 deletions core/apis/assignments/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .student import student_assignments_resources
from .teacher import teacher_assignments_resources
Empty file.
15 changes: 14 additions & 1 deletion core/apis/assignments/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Meta:
unknown = EXCLUDE

id = auto_field(required=False, allow_none=True)
content = auto_field(required=True)
content = auto_field()
created_at = auto_field(dump_only=True)
updated_at = auto_field(dump_only=True)
teacher_id = auto_field(dump_only=True)
Expand All @@ -36,3 +36,16 @@ class Meta:
def initiate_class(self, data_dict, many, partial):
# pylint: disable=unused-argument,no-self-use
return GeneralObject(**data_dict)


class AssignmentGradeSchema(Schema):
class Meta:
unknown = EXCLUDE

id = fields.Integer(required=True, allow_none=False)
grade = EnumField(GradeEnum, required=True, allow_none=False)

@post_load
def initiate_class(self, data_dict, many, partial):
# pylint: disable=unused-argument,no-self-use
return GeneralObject(**data_dict)
8 changes: 4 additions & 4 deletions core/apis/assignments/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


@student_assignments_resources.route('/assignments', methods=['GET'], strict_slashes=False)
@decorators.auth_principal
@decorators.authenticate_principal
def list_assignments(p):
"""Returns list of assignments"""
students_assignments = Assignment.get_assignments_by_student(p.student_id)
Expand All @@ -19,7 +19,7 @@ def list_assignments(p):

@student_assignments_resources.route('/assignments', methods=['POST'], strict_slashes=False)
@decorators.accept_payload
@decorators.auth_principal
@decorators.authenticate_principal
def upsert_assignment(p, incoming_payload):
"""Create or Edit an assignment"""
assignment = AssignmentSchema().load(incoming_payload)
Expand All @@ -33,15 +33,15 @@ def upsert_assignment(p, incoming_payload):

@student_assignments_resources.route('/assignments/submit', methods=['POST'], strict_slashes=False)
@decorators.accept_payload
@decorators.auth_principal
@decorators.authenticate_principal
def submit_assignment(p, incoming_payload):
"""Submit an assignment"""
submit_assignment_payload = AssignmentSubmitSchema().load(incoming_payload)

submitted_assignment = Assignment.submit(
_id=submit_assignment_payload.id,
teacher_id=submit_assignment_payload.teacher_id,
principal=p
auth_principal=p
)
db.session.commit()
submitted_assignment_dump = AssignmentSchema().dump(submitted_assignment)
Expand Down
34 changes: 34 additions & 0 deletions core/apis/assignments/teacher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 .schema import AssignmentSchema, AssignmentGradeSchema
teacher_assignments_resources = Blueprint('teacher_assignments_resources', __name__)


@teacher_assignments_resources.route('/assignments', methods=['GET'], strict_slashes=False)
@decorators.authenticate_principal
def list_assignments(p):
"""Returns list of assignments"""
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)


@teacher_assignments_resources.route('/assignments/grade', methods=['POST'], strict_slashes=False)
@decorators.accept_payload
@decorators.authenticate_principal
def grade_assignment(p, incoming_payload):
"""Grade an assignment"""
grade_assignment_payload = AssignmentGradeSchema().load(incoming_payload)

graded_assignment = Assignment.mark_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)
14 changes: 9 additions & 5 deletions core/apis/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from functools import wraps


class Principal:
def __init__(self, user_id, student_id=None, teacher_id=None):
class AuthPrincipal:
def __init__(self, user_id, student_id=None, teacher_id=None, principal_id=None):
self.user_id = user_id
self.student_id = student_id
self.teacher_id = teacher_id
self.principal_id = principal_id


def accept_payload(func):
Expand All @@ -19,22 +20,25 @@ def wrapper(*args, **kwargs):
return wrapper


def auth_principal(func):
def authenticate_principal(func):
@wraps(func)
def wrapper(*args, **kwargs):
p_str = request.headers.get('X-Principal')
assertions.assert_auth(p_str is not None, 'principal not found')
p_dict = json.loads(p_str)
p = Principal(
p = AuthPrincipal(
user_id=p_dict['user_id'],
student_id=p_dict.get('student_id'),
teacher_id=p_dict.get('teacher_id')
teacher_id=p_dict.get('teacher_id'),
principal_id=p_dict.get('principal_id')
)

if request.path.startswith('/student'):
assertions.assert_true(p.student_id is not None, 'requester should be a student')
elif request.path.startswith('/teacher'):
assertions.assert_true(p.teacher_id is not None, 'requester should be a teacher')
elif request.path.startswith('/principal'):
assertions.assert_true(p.principal_id is not None, 'requester should be a principal')
else:
assertions.assert_found(None, 'No such api')

Expand Down
Empty file added core/apis/teachers/__init__.py
Empty file.
Empty file added core/apis/teachers/principal.py
Empty file.
Empty file added core/apis/teachers/schema.py
Empty file.
8 changes: 4 additions & 4 deletions core/migrations/versions/2087a1db8595_assignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from alembic import op
import sqlalchemy as sa
from core import db
from core.apis.decorators import Principal
from core.apis.decorators import AuthPrincipal
from core.models.users import User
from core.models.students import Student
from core.models.teachers import Teacher
Expand Down Expand Up @@ -82,19 +82,19 @@ def upgrade():
Assignment.submit(
_id=assignment_1.id,
teacher_id=teacher_1.id,
principal=Principal(user_id=student_1.user_id, student_id=student_1.id)
auth_principal=AuthPrincipal(user_id=student_1.user_id, student_id=student_1.id)
)

Assignment.submit(
_id=assignment_3.id,
teacher_id=teacher_2.id,
principal=Principal(user_id=student_2.user_id, student_id=student_2.id)
auth_principal=AuthPrincipal(user_id=student_2.user_id, student_id=student_2.id)
)

Assignment.submit(
_id=assignment_4.id,
teacher_id=teacher_2.id,
principal=Principal(user_id=student_2.user_id, student_id=student_2.id)
auth_principal=AuthPrincipal(user_id=student_2.user_id, student_id=student_2.id)
)

db.session.commit()
Expand Down
46 changes: 46 additions & 0 deletions core/migrations/versions/52a401750a76_principals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""principals
Revision ID: 52a401750a76
Revises: 2087a1db8595
Create Date: 2024-01-07 19:15:22.771993
"""
from alembic import op
import sqlalchemy as sa

from core import db
from core.models.users import User
from core.models.principals import Principal


# revision identifiers, used by Alembic.
revision = '52a401750a76'
down_revision = '2087a1db8595'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('principals',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False),
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
principal_user = User(email='principal@fylebe.com', username='principal')
db.session.add(principal_user)

principal = Principal(user_id=User.get_by_email('principal@fylebe.com').id)

db.session.add(principal)
db.session.commit()
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('principals')
# ### end Alembic commands ###
24 changes: 20 additions & 4 deletions core/models/assignments.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import enum
from core import db
from core.apis.decorators import Principal
from core.apis.decorators import AuthPrincipal
from core.libs import helpers, assertions
from core.models.teachers import Teacher
from core.models.students import Student
Expand Down Expand Up @@ -60,18 +60,34 @@ def upsert(cls, assignment_new: 'Assignment'):
return assignment

@classmethod
def submit(cls, _id, teacher_id, principal: Principal):
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 == principal.student_id, 'This assignment belongs to some other student')
assertions.assert_valid(assignment.student_id == auth_principal.student_id, 'This assignment belongs to some other student')
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


@classmethod
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(grade is not None, 'assignment with empty grade cannot be graded')

assignment.grade = grade
assignment.state = AssignmentStateEnum.GRADED
db.session.flush()

return assignment

@classmethod
def get_assignments_by_student(cls, student_id):
return cls.filter(cls.student_id == student_id).all()

@classmethod
def get_assignments_by_teacher(cls, teacher_id):
return cls.filter(cls.teacher_id == teacher_id).all()
Loading

0 comments on commit d066d7d

Please sign in to comment.