Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internship Backend Task #138

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
env
*.pyc
*.pyo
*.pyd
__pycache__
.pytest_cache
*.db
*.sqlite3
1 change: 1 addition & 0 deletions core/apis/assignments/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .student import student_assignments_resources
from .teacher import teacher_assignments_resources
from .principal import principal_assignments_resources
34 changes: 34 additions & 0 deletions core/apis/assignments/principal.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
principal_assignments_resources = Blueprint('principal_assignments_resources', __name__)

@principal_assignments_resources.route('/assignments', methods=['GET'], strict_slashes=False)
@decorators.authenticate_principal
def get_assignments(p):
"""Lists all submitted and graded assignments"""
all_submitted_and_graded_assignments = Assignment.get_all_submitted_and_graded_assignments()
all_submitted_and_graded_assignments_dump = AssignmentSchema().dump(all_submitted_and_graded_assignments, many=True)
print(all_submitted_and_graded_assignments_dump)
return APIResponse.respond(data=all_submitted_and_graded_assignments_dump)


@principal_assignments_resources.route('/assignments/grade', methods=['POST'], strict_slashes=False)
@decorators.accept_payload
@decorators.authenticate_principal
def grade_or_regrade_assignments(p, incoming_payload):
"""Grades or regrades an assignment by the principal"""
grade_or_regrade_assignment_payload = AssignmentGradeSchema().load(incoming_payload)
graded_or_regraded_assignment = Assignment.mark_grade(
_id=grade_or_regrade_assignment_payload.id,
grade=grade_or_regrade_assignment_payload.grade,
auth_principal=p,
)

db.session.commit()
graded_or_regraded_assignment_dump = AssignmentSchema().dump(graded_or_regraded_assignment)
return APIResponse.respond(data=graded_or_regraded_assignment_dump)
3 changes: 2 additions & 1 deletion core/apis/assignments/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -48,4 +49,4 @@ class Meta:
@post_load
def initiate_class(self, data_dict, many, partial):
# pylint: disable=unused-argument,no-self-use
return GeneralObject(**data_dict)
return GeneralObject(**data_dict)
13 changes: 8 additions & 5 deletions core/apis/assignments/student.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import Blueprint
from flask import Blueprint, jsonify
from core import db
from core.apis import decorators
from core.apis.responses import APIResponse
Expand All @@ -25,10 +25,13 @@ def upsert_assignment(p, incoming_payload):
assignment = AssignmentSchema().load(incoming_payload)
assignment.student_id = p.student_id

upserted_assignment = Assignment.upsert(assignment)
db.session.commit()
upserted_assignment_dump = AssignmentSchema().dump(upserted_assignment)
return APIResponse.respond(data=upserted_assignment_dump)
if assignment.content is not None:
upserted_assignment = Assignment.upsert(assignment)
db.session.commit()
upserted_assignment_dump = AssignmentSchema().dump(upserted_assignment)
return APIResponse.respond(data=upserted_assignment_dump)
else:
return jsonify(error='Content cannot be null'), 400


@student_assignments_resources.route('/assignments/submit', methods=['POST'], strict_slashes=False)
Expand Down
2 changes: 1 addition & 1 deletion core/apis/assignments/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions core/apis/teachers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .principal import principal_teachers_resources
18 changes: 18 additions & 0 deletions core/apis/teachers/principal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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_teachers_resources = Blueprint('principal_teachers_resources', __name__)


@principal_teachers_resources.route('/teachers', methods=['GET'], strict_slashes=False)
@decorators.authenticate_principal
def list_teachers(p):
"""Returns list of teachers"""
teachers_list = Teacher.get_all_teachers()
print(teachers_list[0],end=" ")
teachers_list_dump = TeacherSchema().dump(teachers_list, many=True)
return APIResponse.respond(data=teachers_list_dump)
14 changes: 14 additions & 0 deletions core/apis/teachers/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from marshmallow import Schema, EXCLUDE, fields, 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)
22 changes: 17 additions & 5 deletions core/models/assignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,26 @@ 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
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')

if auth_principal.teacher_id:
assertions.assert_valid(assignment.teacher_id == auth_principal.teacher_id, f'This assign was supposed to be evaluated by {assignment.teacher_id }')
assertions.assert_valid(grade is not None, 'assignment with empty grade cannot be graded')
assertions.assert_valid(assignment.state == AssignmentStateEnum.SUBMITTED, 'Only a submitted assignment can be graded')
elif auth_principal.principal_id:
assertions.assert_valid(assignment.state != AssignmentStateEnum.DRAFT, 'Only a submitted or already graded assignment can be graded')

assignment.grade = grade
assignment.state = AssignmentStateEnum.GRADED
db.session.flush()
Expand All @@ -89,5 +95,11 @@ 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()

@classmethod
def get_all_submitted_and_graded_assignments(cls):
return cls.filter(cls.state == AssignmentStateEnum.SUBMITTED, cls.state == AssignmentStateEnum.GRADED).all()


4 changes: 4 additions & 0 deletions core/models/teachers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ class Teacher(db.Model):

def __repr__(self):
return '<Teacher %r>' % self.id

@classmethod
def get_all_teachers(cls):
return db.session.query(cls).all()
7 changes: 5 additions & 2 deletions core/server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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.apis.teachers import principal_teachers_resources
from core.libs import helpers
from core.libs.exceptions import FyleError
from werkzeug.exceptions import HTTPException
Expand All @@ -10,6 +11,8 @@

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_teachers_resources, url_prefix='/principal')


@app.route('/')
Expand Down Expand Up @@ -41,4 +44,4 @@ def handle_error(err):
error=err.__class__.__name__, message=str(err)
), err.code

raise err
raise err
16 changes: 16 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: "3.12"

services:
web:
build:
context: .
dockerfile: Dockerfile
image: fyle:latest
ports:
- "7755:7755"
volumes:
- .:/app
environment:
- FLASK_ENV=development
- GUNICORN_PORT=7755
command: ["gunicorn", "-c", "gunicorn_config.py", "core.server:app"]
15 changes: 15 additions & 0 deletions dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.8-slim

WORKDIR /app

COPY . /app

RUN pip install --no-cache-dir -r requirements.txt

ENV FLASK_APP=core/server.py

RUN flask db upgrade -d core/migrations/

EXPOSE 7755

CMD ["bash", "run.sh"]
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
-- Write query to find the number of grade A's given by the teacher who has graded the most assignments
WITH teacher as (
SELECT teacher_id, COUNT(*) AS a_grade_count
FROM assignments
WHERE grade IS NOT NULL AND teacher_id IS NOT NULL
GROUP BY teacher_id
ORDER BY a_grade_count DESC
LIMIT 1
)
SELECT count(*) as a_grade_count
FROM teacher JOIN assignments using(teacher_id)
WHERE grade = 'A';
4 changes: 4 additions & 0 deletions tests/SQL/number_of_graded_assignments_for_each_student.sql
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
-- Write query to get number of graded assignments for each student:
SELECT student_id, count(*)
FROM assignments
WHERE state IS 'GRADED'
GROUP BY student_id
33 changes: 33 additions & 0 deletions tests/assertions_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from core.libs import assertions
from core.libs.exceptions import FyleError


def test_assert_auth():
try:
assertions.assert_auth(False)
except FyleError as e:
assert e.status_code == 401
assert e.message == 'UNAUTHORIZED'


def test_assert_true():
try:
assertions.assert_true(False)
except FyleError as e:
assert e.status_code == 403
assert e.message == 'FORBIDDEN'


def test_assert_valid():
try:
assertions.assert_valid(False)
except FyleError as e:
assert e.status_code == 400


def test_assert_found():
try:
assertions.assert_found(None)
except FyleError as e:
assert e.status_code == 404
assert e.message == 'NOT_FOUND'
14 changes: 14 additions & 0 deletions tests/exceptions_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from core.libs.exceptions import FyleError


def test_fyle_error():
error = FyleError(404, 'page not found')
assert error.status_code == 404
assert error.message == 'page not found'


def test_fyle_error_to_dict():
error = FyleError(404, 'page not found')
error_dict = error.to_dict()
assert isinstance(error_dict, dict)
assert error_dict['message'] == 'page not found'
8 changes: 8 additions & 0 deletions tests/principals_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ def test_get_assignments(client, h_principal):
for assignment in data:
assert assignment['state'] in [AssignmentStateEnum.SUBMITTED, AssignmentStateEnum.GRADED]

def test_get_teachers(client, h_principal):
response = client.get(
'/principal/teachers',
headers=h_principal
)

assert response.status_code == 200


def test_grade_assignment_draft_assignment(client, h_principal):
"""
Expand Down
10 changes: 10 additions & 0 deletions tests/server_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def test_index(client):
response = client.get("/")
assert response.status_code == 200
assert response.json["status"] == "ready"


def test_invalid_endpoint(client, h_principal):
response = client.get("/other", headers=h_principal)
assert response.status_code == 404
assert response.json["error"] == "NotFound"
Loading