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

Intern-backend-Assignment #164

Open
wants to merge 9 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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
core/store.sqlite3

__pycache__/
.pytest_cache

env/

htmlcov/
.coverage
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.8.0

WORKDIR /code

ENV FLASK_APP=core/server.py

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

RUN flask db upgrade -d core/migrations/

EXPOSE 7755

CMD ["/bin/bash", "-c", "pytest --cov && coverage html && bash run.sh"]
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
33 changes: 33 additions & 0 deletions core/apis/assignments/principal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 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('/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.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)
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
18 changes: 16 additions & 2 deletions core/apis/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.get_by_id(p.student_id)
assertions.assert_found(student, 'Student does not exist')
user_id_ = student.user_id
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_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_auth(p.user_id == user_id_)
else:
assertions.assert_found(None, 'No such api')

return func(p, *args, **kwargs)
return wrapper
return wrapper
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_teacher_resources
16 changes: 16 additions & 0 deletions core/apis/teachers/principal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from flask import Blueprint
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.get_all_teachers()
teachers_dump = TeacherSchema().dump(teachers, many=True)
return APIResponse.respond(data=teachers_dump)
18 changes: 18 additions & 0 deletions core/apis/teachers/schema.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 0 additions & 2 deletions core/libs/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import random
import string
from datetime import datetime

TIMESTAMP_WITH_TIMEZONE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z'
Expand Down
17 changes: 14 additions & 3 deletions core/models/assignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -64,9 +65,11 @@ 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
Expand All @@ -76,7 +79,10 @@ 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(grade is not None, 'assignment with empty grade cannot be graded')
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
assignment.state = AssignmentStateEnum.GRADED
Expand All @@ -89,5 +95,10 @@ 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_assignments_by_principal(cls):
return cls.filter(cls.state.in_(('GRADED','SUBMITTED'))).all()

9 changes: 9 additions & 0 deletions core/models/principals.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,12 @@ class Principal(db.Model):

def __repr__(self):
return '<Principal %r>' % 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()
9 changes: 9 additions & 0 deletions core/models/students.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,12 @@ class Student(db.Model):

def __repr__(self):
return '<Student %r>' % 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()
13 changes: 13 additions & 0 deletions core/models/teachers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,16 @@ class Teacher(db.Model):

def __repr__(self):
return '<Teacher %r>' % 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()

@classmethod
def get_all_teachers(cls):
return cls.filter().all()
5 changes: 4 additions & 1 deletion 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_teacher_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_teacher_resources, url_prefix='/principal')


@app.route('/')
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: "2.29.1"

services:
backend:
build:
context: ./
container_name: backend_c
ports:
- "7755:7755"
2 changes: 1 addition & 1 deletion run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
7 changes: 6 additions & 1 deletion tests/SQL/number_of_graded_assignments_for_each_student.sql
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion tests/SQL/sql_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions tests/principals_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions tests/students_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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