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

Documentation Added #1

Open
wants to merge 4 commits into
base: master
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 .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions .idea/penngrader.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 65 additions & 3 deletions Backend/grader_lambda.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""
The grader_lambda is the AWS lambda class that will handle test-related events. Its primary responsibility
is to take in an event, parse out key details, such as which homework it's relevant to, which student,
which test case, etc., then handle the actual testing.

The basic flow is:

event happens => lambda_handler is invoked => dynamo DB is updated w/ submission & score
> Examine event
> Get submission details
> Import any libraries
> Test submission
> Log the resulting score
"""



import sys
sys.path.append('/opt')
import os
Expand All @@ -21,6 +38,14 @@


def lambda_handler(event, context):
""" This lambda handler is meant to take in an event and context, then:

1. Parse out the test details: homework_id, student_id, test_case_id, answer
2. Get all of the associated libraries and the test function: test_case, libraries
3. Import those required libraries
4. Score the students work against the test_case: student_score, max_score
5. Log the submission in dynamo
"""
try:
homework_id, student_id, test_case_id, answer = parse_event(event)
test_case, libraries = get_test_and_libraries(homework_id, test_case_id)
Expand All @@ -33,6 +58,9 @@ def lambda_handler(event, context):


def parse_event(event):
""" Simple helper method to parse an event as a dict.
Returns: (homework_id, student_id, test_case_id, answer)
"""
try:
body = ast.literal_eval(event['body'])
return body['homework_id'], \
Expand All @@ -44,6 +72,10 @@ def parse_event(event):


def get_test_and_libraries(homework_id, test_case_id):
""" Retrieve the test function and associated libraries required. This pulls down from dynamo,
then passes back a tuple of (test_case, libraries). It can then be used to import the libraries
and test the student submission
"""
try:
response = dynamo.get_item(TableName = TEST_CASES_TABLE, Key={'homework_id': {'S': homework_id}})
return deserialize(response['Item']['test_cases']['S'])[test_case_id], \
Expand All @@ -53,6 +85,10 @@ def get_test_and_libraries(homework_id, test_case_id):


def import_libraries(libraries): # TO-FINISH #
""" Apparently unfinished, but imports required libraries for a given test. Tests consist of
the function that actually performs the testing and the required libraries to run that function +
the function being tested. This takes care of importing those libraries
"""
try:
packages = libraries['packages']
imports = libraries['imports']
Expand All @@ -78,6 +114,10 @@ def import_libraries(libraries): # TO-FINISH #


def grade(test_case, answer):
""" Generate a grade for a given test case. test_case is a function that is passed in. The function
gets executed here using answer as its param. This will catch any errors in the code and returns
a tuple of (score, max_score)
"""
try:
return test_case(answer)
except Exception as exception:
Expand All @@ -87,6 +127,16 @@ def grade(test_case, answer):


def store_submission(student_score, max_score, homework_id, test_case_id, student_id):
""" Log the student submission in the database.
params:
- student_score: the number of points the student received on a problem
- max_score: the maximum score on that problem
- homeword_id: the key for this particular homework
- test_case_id: the key for this particular test case (problem)
- student_id: the key for the student

This will be passed to the dynamo DB as a new record
"""
try:
db_entry = {
'TableName': GRADEBOOK_TABLE,
Expand Down Expand Up @@ -116,25 +166,37 @@ def store_submission(student_score, max_score, homework_id, test_case_id, studen


def serialize(obj):
""" Simple helper method to encode a serialized object
"""
byte_serialized = dill.dumps(obj, recurse = True)
return base64.b64encode(byte_serialized).decode("utf-8")


def deserialize(obj):
""" Simple helper method to decode a base64 encoded object
"""
byte_decoded = base64.b64decode(obj)
return dill.loads(byte_decoded)


def build_response_message(student_score, max_score):
def build_response_message(student_score, max_score, msg=None):
""" Build the string response message
"""
out = ""
if student_score == max_score:
return 'Correct! You earned {}/{} points. You are a star!\n\n'.format(student_score, max_score) + \
out = 'Correct! You earned {}/{} points. You are a star!\n\n'.format(student_score, max_score) + \
'Your submission has been successfully recorded in the gradebook.'
else:
return 'You earned {}/{} points.\n\n'.format(student_score, max_score) + \
out = 'You earned {}/{} points.\n\n'.format(student_score, max_score) + \
'But, don\'t worry you can re-submit and we will keep only your latest score.'
if msg is not None:
out += "\n\n{}".format(msg)
return out


def build_http_response(status_code, message):
""" Build formatted http response as string
"""
return {
'statusCode': status_code,
'body': str(message),
Expand Down
46 changes: 46 additions & 0 deletions Backend/grades_lambda.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
"""
The grades_lambda is the AWS lambda class that will handle grade-related events. Its primary responsibility
is to take in an event, parse out key details, such as which homework it's relevant to and whether an
individual student or all students, then return grading details for that homework.

The basic flow is:

event happens => lambda_handler is invoked => dynamo DB is updated w/ submission & score
> Examine event
> Determine scope (single vs.
all students)
> Return http response
"""

import sys
sys.path.append('/opt')
import os
Expand Down Expand Up @@ -31,6 +45,16 @@


def lambda_handler(event, context):
""" This lambda handler is meant to take in an event and context, then:

1. Parse out the event details, then get the ID of the homework: body, homework_id
2. Get all associated metadata for the homework: deadline, max_daily_submissions, max_score
3. Check the request type:
> ALL_STUDENTS_REQUEST: validate the secret, then retrieve the grades and return http response of
(all_grades, deadline)
> STUDENT_REQUEST: retrieve a single student's grade for an assignment, return http response of
(grades, deadline, max_daily_submissions, max_score)
"""
try:
body = parse_event(event)
homework_id = body['homework_id']
Expand All @@ -51,13 +75,18 @@ def lambda_handler(event, context):


def parse_event(event):
""" Simple helper method to parse an event as a dict.
Returns: entire event
"""
try:
return ast.literal_eval(event['body'])
except:
raise Exception('Malformed payload.')


def validate_secret_key(secret_key):
""" Simple method to confirm that the secret_key passed in is valid
"""
try:
response = dynamo.get_item(TableName = 'Classes', Key={'secret_key': {'S': secret_key}})
return response['Item']['course_id']['S']
Expand All @@ -66,6 +95,8 @@ def validate_secret_key(secret_key):


def get_homework_metadata(homework_id):
""" Retrieves the metadata for a homework, including deadline, max_daily_subimssions, and the total_score
"""
try:
response = dynamo.get_item(TableName = METADATA_TABLE, Key={'homework_id': {'S': homework_id}})
return response['Item']['deadline']['S'], \
Expand All @@ -76,6 +107,11 @@ def get_homework_metadata(homework_id):


def get_grades(homework_id, student_id = None):
""" Retrieve the grades for a given homework using the student's ID. This will hit the
dynamo DB and pull down the student's current score on a particular assignment.

Note: if student_id is None, grades for all students are returned
"""
table = dynamo_resource.Table(GRADEBOOK_TABLE)
if student_id is not None:
filtering_exp = Key('homework_id').eq(homework_id) & Attr('student_submission_id').begins_with(student_id)
Expand All @@ -87,11 +123,21 @@ def get_grades(homework_id, student_id = None):


def serialize(obj):
""" Simple helper method to encode a serialized object

Code is duplicated here. Possibly because the other file may not always be
accessible?
"""
byte_serialized = dill.dumps(obj, recurse = True)
return base64.b64encode(byte_serialized).decode("utf-8")


def build_http_response(status_code, message):
""" Build formatted http response as string

Code is duplicated here. Possibly because the other file may not always be
accessible?
"""
return {
'statusCode': status_code,
'body': str(message),
Expand Down
Loading