Commit 9918b7a2 by Ibrahim Awwal

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

parent 559e4710
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import requests import requests
import slumber import slumber
import simplejson as json import simplejson as json
from django.core.serializers.json import DjangoJSONEncoder
# HOST = getattr(settings, 'GRADING_SERVICE_HOST', 'http://localhost:3000/') # HOST = getattr(settings, 'GRADING_SERVICE_HOST', 'http://localhost:3000/')
# The client library *should* be django independent. Django client should override this value somehow. # The client library *should* be django independent. Django client should override this value somehow.
...@@ -40,7 +41,7 @@ class APIModel(object): ...@@ -40,7 +41,7 @@ class APIModel(object):
return self.__base_url__ return self.__base_url__
def update_attributes(self, **kwargs): def update_attributes(self, **kwargs):
if 'id' in kwargs: if 'id' in kwargs and kwargs['id'] is not None:
self._id = int(kwargs['id']) self._id = int(kwargs['id'])
for attribute in self.__attributes__: for attribute in self.__attributes__:
if attribute in kwargs: if attribute in kwargs:
...@@ -48,7 +49,7 @@ class APIModel(object): ...@@ -48,7 +49,7 @@ class APIModel(object):
def to_json(self): def to_json(self):
attributes = dict([(key, getattr(self,key, None)) for key in self.__attributes__ if hasattr(self, key)]) 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): def save(self):
# TODO: Think of a better way to handle nested resources, currently you have to manually set __base_url__ # 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): ...@@ -64,7 +65,11 @@ class APIModel(object):
else: else:
# TODO: handle errors # TODO: handle errors
print response 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 print self.errors
return self return self
...@@ -78,6 +83,7 @@ class APIModel(object): ...@@ -78,6 +83,7 @@ class APIModel(object):
@property @property
def json_root(self): def json_root(self):
return self.__class__.__name__.lower() return self.__class__.__name__.lower()
@property @property
def id(self): def id(self):
""" """
...@@ -107,20 +113,20 @@ class User(APIModel): ...@@ -107,20 +113,20 @@ class User(APIModel):
class Question(APIModel): class Question(APIModel):
__attributes__ = ['external_id', 'rubric_id', 'total_points'] __attributes__ = ['external_id', 'rubric_id', 'total_points']
__slots__ = __attributes__ __slots__ = __attributes__ + ['_grading_configuration']
__base_url__ = 'questions' __base_url__ = 'questions'
def submissions(self): def submissions(self):
return [Submission(**data) for data in API.questions(id).submissions.get()] return [Submission(**data) for data in API.questions(id).submissions.get()]
@property @property
def grading_queue(self): def grading_queue(self):
return GradingQueue(self, question_id=self.id) return GradingQueue(self)
@property @property
def grading_configuration(self): def grading_configuration(self):
if not self._grading_configuration: if not hasattr(self, '_grading_configuration'):
# Try to query the service for the 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: if response.status_code == 200:
self._grading_configuration = GradingConfiguration(**response.json) self._grading_configuration = GradingConfiguration(**response.json)
else: else:
...@@ -185,16 +191,15 @@ class Rubric(APIModel): ...@@ -185,16 +191,15 @@ class Rubric(APIModel):
self.entries.append(entry) self.entries.append(entry)
return 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 # 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 = Evaluation(rubric_id=self.id, user_id=user_id, submission_id=submission_id)
evaluation.save()
for entry in self.entries: for entry in self.entries:
present = False present = False
if entry.id in entry_values: if entry.id in entry_values:
present = entry_values[entry.id] present = entry_values[entry.id]
value = RubricEntryValue(rubric_entry_id=entry.id, evaluation_id=evaluation.id, present=present) evaluation.add_entry(entry.id, present)
value.save() evaluation.save()
return evaluation return evaluation
class RubricEntry(APIModel): class RubricEntry(APIModel):
...@@ -218,21 +223,22 @@ class GradingConfiguration(APIModel): ...@@ -218,21 +223,22 @@ class GradingConfiguration(APIModel):
__base_url__ = 'grading_configuration' __base_url__ = 'grading_configuration'
def url(self): def url(self):
return slumber.url_join('question', self.question_id, 'grading_configuration') return slumber.url_join('questions', self.question_id, 'grading_configuration')
@property @property
def json_root(self): def json_root(self):
return 'grading_configuration' return 'grading_configuration'
class Group(APIModel): class Group(APIModel):
__attributes__ = ['title'] __attributes__ = ['title']
__slots__ = __attributes__ __slots__ = __attributes__ + ['memberships']
__base_url__ = 'groups' __base_url__ = 'groups'
def __init__(self, **kwargs): def __init__(self, **kwargs):
memberships = kwargs.pop('memberships', None) memberships = kwargs.pop('memberships', None)
self.update_attributes(**kwargs) self.update_attributes(**kwargs)
if memberships: if memberships:
print memberships print memberships
self.memberships = [GroupMembership(**data) for data in memberships] self.save()
self.memberships = [GroupMembership(**data).save() for data in memberships]
else: else:
self.memberships = [] self.memberships = []
...@@ -241,58 +247,98 @@ class Group(APIModel): ...@@ -241,58 +247,98 @@ class Group(APIModel):
self.memberships = [GroupMembership(**data) for data in memberships] self.memberships = [GroupMembership(**data) for data in memberships]
def add_user(self, user): 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): 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 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 a user from a list. This operation could be done much more easily on the service side.
controller less RESTful.
""" """
membership_id = next((x.id for x in self.memberships if x.user_id == user.id), None) membership_id = next((x.id for x in self.memberships if x.user_id == user.id), None)
if membership_id: if membership_id:
API.groups(self.id).memberships(membership_id).delete() API.groups(self.id).memberships(membership_id).delete()
@staticmethod @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'))) g = Group(**API.groups(id).get(include_members=('1' if include_members else '0')))
return g return g
class GroupMembership(APIModel): class GroupMembership(APIModel):
__attributes__ = ['user_id', 'group_id', 'name'] __attributes__ = ['user_id', 'group_id']
__slots__ = __attributes__ __slots__ = __attributes__
__base_url__ = 'memberships' __base_url__ = 'memberships'
@property @property
def json_root(self): 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): class GroupRole(APIModel):
__attributes__ = ['grading_configuration_id', 'group_id', 'role'] __attributes__ = ['question_id', 'group_id', 'role']
__slots__ = __attributes__ __slots__ = __attributes__
__base_url__ = 'group_roles' __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 @property
def json_root(self): 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): class Example(APIModel):
__attributes__ = ['grading_configuration_id', 'submission_id', 'user_id'] __attributes__ = ['gquestion_id', 'submission_id', 'user_id']
__slots__ = __attributes__ __slots__ = __attributes__
__base_url__ = 'examples' __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): class Evaluation(APIModel):
__attributes__ = ['rubric_id', 'user_id', 'submission_id', 'comments', 'offset', 'question_id'] __attributes__ = ['rubric_id', 'user_id', 'submission_id', 'comments', 'offset']
__slots__ = __attributes__ + ['entries'] # this is an ugly hack __slots__ = __attributes__ + ['entries']
__base_url = 'evaluations' __base_url = 'evaluations'
def url(self): def url(self):
if self.id: if self.id:
return slumber.url_join('questions', self.question_id, 'submissions', self.submission_id, 'evaluations') return slumber.url_join('evaluations', self.id)
else: else:
return slumber.url_join('questions', self.question_id, 'submissions', self.submission_id, 'evaluations', self.id) return slumber.url_join('evaluations')
def add_entry(self):
if self.entries is None: def add_entry(self, rubric_entry_id, value):
if not hasattr(self, 'entries'):
self.entries = {} self.entries = {}
self.entries[rubric_entry_id]=value
def to_json(self): def to_json(self):
attributes = dict([(key, getattr(self,key, None)) for key in self.__attributes__ if hasattr(self, key)]) 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 entries_attributes = []
return json.dumps({self.json_root: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): class RubricEntryValue(APIModel):
""" """
...@@ -311,10 +357,15 @@ class Task(APIModel): ...@@ -311,10 +357,15 @@ class Task(APIModel):
class GradingQueue(APIModel): class GradingQueue(APIModel):
__attributes__ = ['question_id'] __attributes__ = ['question_id']
def __init__(self, question, grading_configuration, **kwargs): def __init__(self, question, **kwargs):
self.question = question 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): 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} params = {'user_id':user.id}
response = requests.post(url, params) response = requests.post(url, params)
if response.status_code == 200: if response.status_code == 200:
......
...@@ -7,18 +7,18 @@ from grading_client.api import * ...@@ -7,18 +7,18 @@ from grading_client.api import *
# Create 30 local students, 100 remote students, 2 instructors, and 5 graders. # 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)] 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)] 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)] 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)] graders = [User(name="Grader %d"%x, external_id="edx:%d"%(x+100)).save() for x in xrange(num_graders)]
# Create 5 questions # Create 5 questions
num_questions = 5 num_questions = 3
questions = {} questions = {}
group_names = ['local', 'remote1'] group_names = ['local', 'remote1']
for variant in group_names: 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 # Submit submissions for all users
# Keep track of a "ground-truth" value for the scoring somehow # Keep track of a "ground-truth" value for the scoring somehow
...@@ -28,13 +28,13 @@ local_submissions = {} ...@@ -28,13 +28,13 @@ local_submissions = {}
# local_submissions_true_scores = np.ndarray((num_local, num_questions, 3), dtype=np.bool) # local_submissions_true_scores = np.ndarray((num_local, num_questions, 3), dtype=np.bool)
local_true_scores = {} local_true_scores = {}
for question in questions['local']: 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] 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]: for submission in local_submissions[question]:
submission.save() submission.save()
m1 = (random() > 0.8) m1 = (random() > 0.8)
m2 = (random() > 0.7) m2 = (random() > 0.7)
correct = not (m1 or m2) correct = not (m1 or m2)
local_true_scores[submission.id] = (m1, m2, correct) local_true_scores[submission.id] = (m1, m2, correct)
# for user_index in xrange(num_local): # for user_index in xrange(num_local):
# for question_index in xrange(num_questions): # for question_index in xrange(num_questions):
...@@ -48,9 +48,9 @@ for question in questions['local']: ...@@ -48,9 +48,9 @@ for question in questions['local']:
remote_submissions = {} remote_submissions = {}
#remote_submissions_true_scores = np.ndarray((num_remote, num_questions, 3), dtype=np.bool) #remote_submissions_true_scores = np.ndarray((num_remote, num_questions, 3), dtype=np.bool)
for question in questions['remote1']: 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] 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]: for submission in remote_submissions[question]:
submission.save() submission.save()
# Instructor creates rubric # Instructor creates rubric
...@@ -64,41 +64,50 @@ rubric.save() # Saves all the entries ...@@ -64,41 +64,50 @@ rubric.save() # Saves all the entries
# This doesn't quite get the interleaving of rubric creation and evaluation, but # This doesn't quite get the interleaving of rubric creation and evaluation, but
# it shouldn't matter in practice # 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']] 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 # Create group for graders
grader_group = Group(title='Local Graders').save() grader_group = Group(title='Local Graders').save()
for user in graders: 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: for config in local_configurations:
config.evaluations_per_submission = 1 config.evaluations_per_submission = 1
config.evaluations_per_grader = num_local / num_graders config.evaluations_per_grader = num_local / num_graders
config.training_exercises_required = 0 config.training_exercises_required = 0
config.open_date = datetime.datetime.now() config.open_date = datetime.datetime.now()
config.due_date = datetime.datetime.now() # TODO FIX config.due_date = datetime.datetime.now() # TODO FIX
config.save() 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. # Now readers sign in and get work. Readers are also accurate in grading.
queue = question.grading_queue
for user in graders: for user in graders:
for question, config in zip(questions['local'], local_configurations): for question, config in zip(questions['local'], local_configurations):
tasks = queue.request_work_for_user(user) queue = question.grading_queue
for task in tasks: tasks = queue.request_work_for_user(user)
submission = Submission.get_by_question_id_and_id(question.id, task.submission_id) for task in tasks:
entries_dict = { entry.id:value for entry, value in zip(rubric.entries, local_true_scores[submission.id]) } submission = Submission.get_by_question_id_and_id(question.id, task.submission_id)
evaluation = rubric.create_evaluation(user_id=user.id, submission_id=submission.id, entry_values=entries_dict) 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() #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