Commit 9918b7a2 by Ibrahim Awwal

Client library updates, grading functionality should all be working. Needs unit tests.

parent 559e4710
......@@ -3,6 +3,7 @@
import requests
import slumber
import simplejson as json
from django.core.serializers.json import DjangoJSONEncoder
# HOST = getattr(settings, 'GRADING_SERVICE_HOST', 'http://localhost:3000/')
# The client library *should* be django independent. Django client should override this value somehow.
......@@ -40,7 +41,7 @@ class APIModel(object):
return self.__base_url__
def update_attributes(self, **kwargs):
if 'id' in kwargs:
if 'id' in kwargs and kwargs['id'] is not None:
self._id = int(kwargs['id'])
for attribute in self.__attributes__:
if attribute in kwargs:
......@@ -48,7 +49,7 @@ class APIModel(object):
def to_json(self):
attributes = dict([(key, getattr(self,key, None)) for key in self.__attributes__ if hasattr(self, key)])
return json.dumps({self.json_root:attributes})
return json.dumps({self.json_root:attributes}, cls=DjangoJSONEncoder)
def save(self):
# TODO: Think of a better way to handle nested resources, currently you have to manually set __base_url__
......@@ -64,7 +65,11 @@ class APIModel(object):
else:
# TODO: handle errors
print response
self.errors = response.json['errors']
print response.text
if response.status_code == 404:
raise Exception("404 Not Found")
if response.json:
self.errors = response.json['errors']
print self.errors
return self
......@@ -78,6 +83,7 @@ class APIModel(object):
@property
def json_root(self):
return self.__class__.__name__.lower()
@property
def id(self):
"""
......@@ -107,20 +113,20 @@ class User(APIModel):
class Question(APIModel):
__attributes__ = ['external_id', 'rubric_id', 'total_points']
__slots__ = __attributes__
__slots__ = __attributes__ + ['_grading_configuration']
__base_url__ = 'questions'
def submissions(self):
return [Submission(**data) for data in API.questions(id).submissions.get()]
@property
def grading_queue(self):
return GradingQueue(self, question_id=self.id)
return GradingQueue(self)
@property
def grading_configuration(self):
if not self._grading_configuration:
if not hasattr(self, '_grading_configuration'):
# Try to query the service for the grading configuration
response = requests.get(slumber.url_join('question', self.id, 'grading_configuration'))
response = requests.get(slumber.url_join(HOST, 'questions', self.id, 'grading_configuration'))
if response.status_code == 200:
self._grading_configuration = GradingConfiguration(**response.json)
else:
......@@ -185,16 +191,15 @@ class Rubric(APIModel):
self.entries.append(entry)
return entry
def create_evaluation(self, user_id, question_id, submission_id, entry_values):
def create_evaluation(self, user_id, submission_id, entry_values):
# TODO: When async API is implemented, entries should be created in a callback
evaluation = Evaluation(rubric_id=self.id, user_id=user_id, submission_id=submission_id)
evaluation.save()
for entry in self.entries:
present = False
if entry.id in entry_values:
present = entry_values[entry.id]
value = RubricEntryValue(rubric_entry_id=entry.id, evaluation_id=evaluation.id, present=present)
value.save()
evaluation.add_entry(entry.id, present)
evaluation.save()
return evaluation
class RubricEntry(APIModel):
......@@ -218,21 +223,22 @@ class GradingConfiguration(APIModel):
__base_url__ = 'grading_configuration'
def url(self):
return slumber.url_join('question', self.question_id, 'grading_configuration')
return slumber.url_join('questions', self.question_id, 'grading_configuration')
@property
def json_root(self):
return 'grading_configuration'
class Group(APIModel):
__attributes__ = ['title']
__slots__ = __attributes__
__slots__ = __attributes__ + ['memberships']
__base_url__ = 'groups'
def __init__(self, **kwargs):
memberships = kwargs.pop('memberships', None)
self.update_attributes(**kwargs)
if memberships:
print memberships
self.memberships = [GroupMembership(**data) for data in memberships]
self.save()
self.memberships = [GroupMembership(**data).save() for data in memberships]
else:
self.memberships = []
......@@ -241,58 +247,98 @@ class Group(APIModel):
self.memberships = [GroupMembership(**data) for data in memberships]
def add_user(self, user):
return GroupMembership(**API.groups(self.id).memberships.post(user_id=user.id))
membership = GroupMembership(group_id=self.id, user_id=user.id)
membership.save()
return membership
#GroupMembership(**API.groups(self.id).memberships.post({'membership':{'user_id':user.id}}))
def remove_user(self, user):
"""
The proper way to remove a user from a group would be to have the membership_id ahead of time by eg. clicking on
a user from a list. This operation could be done much more easily on the service side but it would make the
controller less RESTful.
a user from a list. This operation could be done much more easily on the service side.
"""
membership_id = next((x.id for x in self.memberships if x.user_id == user.id), None)
if membership_id:
API.groups(self.id).memberships(membership_id).delete()
@staticmethod
def get_by_id(id, include_members=False):
def get_by_id(id, include_members=True):
g = Group(**API.groups(id).get(include_members=('1' if include_members else '0')))
return g
class GroupMembership(APIModel):
__attributes__ = ['user_id', 'group_id', 'name']
__attributes__ = ['user_id', 'group_id']
__slots__ = __attributes__
__base_url__ = 'memberships'
@property
def json_root(self):
return 'group_membership'
return 'membership'
def url(self):
if self.id:
return slumber.url_join('groups', self.group_id, 'memberships', self.id)
else:
return slumber.url_join('groups', self.group_id, 'memberships')
def to_json(self):
attributes = dict([(key, getattr(self,key, None)) for key in self.__attributes__ if hasattr(self, key)])
attributes.pop('group_id', None)
return json.dumps({self.json_root:attributes})
class GroupRole(APIModel):
__attributes__ = ['grading_configuration_id', 'group_id', 'role']
__attributes__ = ['question_id', 'group_id', 'role']
__slots__ = __attributes__
__base_url__ = 'group_roles'
(SUBMITTER, GRADER, ADMIN) = (0, 1, 2)
def url(self):
if self.id:
return slumber.url_join('questions', self.question_id, 'group_roles', self.id)
else:
return slumber.url_join('questions', self.question_id, 'group_roles')
@property
def json_root(self):
return 'group_role'
return 'group_role'
def to_json(self):
attributes = dict([(key, getattr(self,key, None)) for key in self.__attributes__ if hasattr(self, key)])
attributes.pop('question_id', None)
return json.dumps({self.json_root: attributes})
class Example(APIModel):
__attributes__ = ['grading_configuration_id', 'submission_id', 'user_id']
__attributes__ = ['gquestion_id', 'submission_id', 'user_id']
__slots__ = __attributes__
__base_url__ = 'examples'
def url(self):
if self.id:
return slumber.url_join('questions'. self.question_id, 'examples', self.id)
else:
return slumber.url_join('questions'. self.question_id, 'examples')
@property
def json_root(self):
return 'example'
def to_json(self):
attributes = dict([(key, getattr(self,key, None)) for key in self.__attributes__ if hasattr(self, key)])
attributes.pop('question_id', None)
return json.dumps({self.json_root: attributes})
class Evaluation(APIModel):
__attributes__ = ['rubric_id', 'user_id', 'submission_id', 'comments', 'offset', 'question_id']
__slots__ = __attributes__ + ['entries'] # this is an ugly hack
__attributes__ = ['rubric_id', 'user_id', 'submission_id', 'comments', 'offset']
__slots__ = __attributes__ + ['entries']
__base_url = 'evaluations'
def url(self):
if self.id:
return slumber.url_join('questions', self.question_id, 'submissions', self.submission_id, 'evaluations')
return slumber.url_join('evaluations', self.id)
else:
return slumber.url_join('questions', self.question_id, 'submissions', self.submission_id, 'evaluations', self.id)
def add_entry(self):
if self.entries is None:
return slumber.url_join('evaluations')
def add_entry(self, rubric_entry_id, value):
if not hasattr(self, 'entries'):
self.entries = {}
self.entries[rubric_entry_id]=value
def to_json(self):
attributes = dict([(key, getattr(self,key, None)) for key in self.__attributes__ if hasattr(self, key)])
attributes.pop('question_id', None) # Remove question_id from params
return json.dumps({self.json_root:attributes})
entries_attributes = []
for entry_id, value in self.entries.items():
entries_attributes.append({'rubric_entry_id': entry_id, 'present': value})
return json.dumps({self.json_root: attributes, 'entries_attributes':entries_attributes})
class RubricEntryValue(APIModel):
"""
......@@ -311,10 +357,15 @@ class Task(APIModel):
class GradingQueue(APIModel):
__attributes__ = ['question_id']
def __init__(self, question, grading_configuration, **kwargs):
def __init__(self, question, **kwargs):
self.question = question
self.question_id = question.id
def url(self):
return slumber.url_join(HOST, 'questions', self.question_id, 'grading_queue')
def request_work_for_user(self, user):
url = slumber.url_join('questions', self.question_id, 'grading_queue', 'request_work')
# TODO: Move this to grading_queue_controller? More sensical that way
url = slumber.url_join(HOST, 'questions', self.question_id, 'tasks', 'request_work')
params = {'user_id':user.id}
response = requests.post(url, params)
if response.status_code == 200:
......
......@@ -7,18 +7,18 @@ from grading_client.api import *
# Create 30 local students, 100 remote students, 2 instructors, and 5 graders.
num_local, num_remote, num_instructors, num_graders = (30, 100, 2, 5)
num_local, num_remote, num_instructors, num_graders = (30, 10, 2, 5)
local_students = [User(name="Student %d"%x, external_id="calx:%d"%(x+2000)).save() for x in xrange(num_local)]
remote_students = [User(name="Student %d"%x, external_id="edx:%d"%(x+1000)).save() for x in xrange(num_remote)]
instructors = [User(name="Instructor %d"%x, external_id="edx:%d"%x).save() for x in xrange(num_instructors)]
graders = [User(name="Grader %d"%x, external_id="edx:%d"%(x+100)).save() for x in xrange(num_graders)]
# Create 5 questions
num_questions = 5
num_questions = 3
questions = {}
group_names = ['local', 'remote1']
for variant in group_names:
questions[variant] = [Question(external_id="calx_q:%d"%x, total_points=2, due_date=datetime.datetime.now()).save() for x in xrange(num_questions)]
questions[variant] = [Question(external_id="calx_q:%d"%x, total_points=2, due_date=datetime.datetime.now()).save() for x in xrange(num_questions)]
# Submit submissions for all users
# Keep track of a "ground-truth" value for the scoring somehow
......@@ -28,13 +28,13 @@ local_submissions = {}
# local_submissions_true_scores = np.ndarray((num_local, num_questions, 3), dtype=np.bool)
local_true_scores = {}
for question in questions['local']:
local_submissions[question] = [(Submission(question_id=question.id, user_id=user.id, external_id="calx_s:%d"%(user.id+1000*question.id))) for user in local_students]
for submission in local_submissions[question]:
submission.save()
m1 = (random() > 0.8)
m2 = (random() > 0.7)
correct = not (m1 or m2)
local_true_scores[submission.id] = (m1, m2, correct)
local_submissions[question] = [(Submission(question_id=question.id, user_id=user.id, external_id="calx_s:%d"%(user.id+1000*question.id))) for user in local_students]
for submission in local_submissions[question]:
submission.save()
m1 = (random() > 0.8)
m2 = (random() > 0.7)
correct = not (m1 or m2)
local_true_scores[submission.id] = (m1, m2, correct)
# for user_index in xrange(num_local):
# for question_index in xrange(num_questions):
......@@ -48,9 +48,9 @@ for question in questions['local']:
remote_submissions = {}
#remote_submissions_true_scores = np.ndarray((num_remote, num_questions, 3), dtype=np.bool)
for question in questions['remote1']:
remote_submissions[question] = [Submission(question_id=question.id, user_id=user.id, external_id="edx_s:%d"%(user.id+1000*question.id)) for user in remote_students]
for submission in remote_submissions[question]:
submission.save()
remote_submissions[question] = [Submission(question_id=question.id, user_id=user.id, external_id="edx_s:%d"%(user.id+1000*question.id)) for user in remote_students]
for submission in remote_submissions[question]:
submission.save()
# Instructor creates rubric
......@@ -64,41 +64,50 @@ rubric.save() # Saves all the entries
# This doesn't quite get the interleaving of rubric creation and evaluation, but
# it shouldn't matter in practice
inst1 = instructors[0]
instructor_evals = []
for question in questions['local']:
for submission in local_submissions[question][:5]:
entries_dict = { entry.id:value for entry, value in zip(rubric.entries, local_true_scores[submission.id]) }
evaluation = rubric.create_evaluation(user_id=inst1.id, question_id=question.id, submission_id=submission.id, entry_values=entries_dict)
#evaluation.save()
instructor_evals.append(evaluation)
local_configurations = [question.grading_configuration for question in questions['local']]
# Create group for instructors
instructor_group = Group(title='Local Instructors').save()
for user in instructors:
instructor_group.add_user(user)
# Create group for graders
grader_group = Group(title='Local Graders').save()
for user in graders:
grader_group.add_user(user)
grader_group.add_user(user)
# Configure grading for readers
# Configure grading for readers and instructors
for config in local_configurations:
config.evaluations_per_submission = 1
config.evaluations_per_grader = num_local / num_graders
config.training_exercises_required = 0
config.open_date = datetime.datetime.now()
config.due_date = datetime.datetime.now() # TODO FIX
config.save()
config.evaluations_per_submission = 1
config.evaluations_per_grader = num_local / num_graders
config.training_exercises_required = 0
config.open_date = datetime.datetime.now()
config.due_date = datetime.datetime.now() # TODO FIX
config.save()
admin_role = GroupRole(group_id=instructor_group.id, question_id=config.question_id, role=GroupRole.ADMIN)
admin_role.save()
grader_role = GroupRole(group_id=grader_group.id, question_id=config.question_id,role=GroupRole.GRADER)
grader_role.save()
inst1 = instructors[0]
instructor_evals = []
for question in questions['local']:
for submission in local_submissions[question][:5]:
entries_dict = {entry.id:value for entry, value in zip(rubric.entries, local_true_scores[submission.id])}
evaluation = rubric.create_evaluation(user_id=inst1.id, submission_id=submission.id, entry_values=entries_dict)
#evaluation.save()
instructor_evals.append(evaluation)
role = GroupRole(group_id=grader_group.id,grading_configuration_id=config.id,role=1)
role.save()
# Now readers sign in and get work. Readers are also accurate in grading.
queue = question.grading_queue
for user in graders:
for question, config in zip(questions['local'], local_configurations):
tasks = queue.request_work_for_user(user)
for task in tasks:
submission = Submission.get_by_question_id_and_id(question.id, task.submission_id)
entries_dict = { entry.id:value for entry, value in zip(rubric.entries, local_true_scores[submission.id]) }
evaluation = rubric.create_evaluation(user_id=user.id, submission_id=submission.id, entry_values=entries_dict)
for question, config in zip(questions['local'], local_configurations):
queue = question.grading_queue
tasks = queue.request_work_for_user(user)
for task in tasks:
submission = Submission.get_by_question_id_and_id(question.id, task.submission_id)
entries_dict = { entry.id:value for entry, value in zip(rubric.entries, local_true_scores[submission.id]) }
evaluation = rubric.create_evaluation(user_id=user.id, submission_id=submission.id, entry_values=entries_dict)
#evaluation.save()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment