From 7ab1325647f2a4f9e2fcbb3257aeffbcac317a4d Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Wed, 2 Jan 2013 13:34:14 +0200 Subject: [PATCH] extended grader --- common/lib/capa/capa/graders/draganddrop.py | 232 ++++++++++++++++---- 1 file changed, 184 insertions(+), 48 deletions(-) diff --git a/common/lib/capa/capa/graders/draganddrop.py b/common/lib/capa/capa/graders/draganddrop.py index 1e847b105b..eaee4bc58e 100644 --- a/common/lib/capa/capa/graders/draganddrop.py +++ b/common/lib/capa/capa/graders/draganddrop.py @@ -28,74 +28,210 @@ values are (x,y) coordinates of centers of dragged images. """ import json +from collections import OrderedDict -def grade(user_input, correct_answer): - ''' - Grade drag and drop problem. - If use_targets is True - checks if image placed on proper target. - If use_targets is False - checks if image placed on proper coordinates, - with setted radius of forgiveness (default is 10) +class PositionsCompare(list): + """Inputs are: "abc" - target + [10, 20] - list of integers + [[10,20], 200] list of list and integer - Args: - user_input, correct_answer: json. Format: + """ + def __eq__(self, other): + # Default lists behaviour is convers "abc" to ["a", "b", "c"]. + # We will use that. + # import ipdb; ipdb.set_trace() - user_input: see module docstring + #check if self or other is not empty list (empty lists = false) + if not self or not other: + return False - correct_answer: - if use_targets is True: - {'1': 't1', 'name_with_icon': 't2'} - else: - {'1': '[10, 10]', 'name_with_icon': '[[10, 10], 20]'} + # check correct input types + if (not isinstance(self[0], (str, unicode, list, int)) or + not isinstance(other[0], (str, unicode, list, int))): + print 'Incorrect input type' + return False - Returns: - True or False. - ''' + if (isinstance(self[0], (list, int)) and + isinstance(other[0], (list, int))): + print 'Numerical position compare' + return self.coordinate_positions_compare(other) + elif (isinstance(self[0], (unicode, str)) and + isinstance(other[0], (unicode, str))): + print 'Targets compare' + return ''.join(self) == ''.join(other) + else: + # we do not have ints or lists of lists or two string/unicode lists + # on both sides + print type(self[0]), type(other[0]), "not correct" + return False - user_answer = json.loads(user_input) + def __ne__(self, other): + return not self.__eq__(other) - if len(correct_answer.keys()) != len(user_answer['draggables']): - return False - - def is_equal(user_answer, correct_answer): - """ Checks if user_answer is equal to correct_answer inside radius + def coordinate_positions_compare(self, other, r=10): + """ Checks if pos1 is equal to pos2 inside radius of forgiveness (default 10 px). Args: - user_answer: [x, y] - list of floats; - correct_answer: [x, y] or [[x, y], r], where - r is radius of forgiveness; + self, other: [x, y] or [[x, y], r], where + r is radius of forgiveness; + x, y, r: int Returns: bool. """ - if not isinstance(correct_answer, list) or isinstance(user_answer, list): - return False + print 'I am called', self, other - r = 10 - if isinstance(correct_answer[0], list): # [(x, y), r] case - r = correct_answer[1] - corr_x = correct_answer[0][0] - corr_y = correct_answer[0][1] - else: # (x, y) case - corr_x = correct_answer[0] - corr_y = correct_answer[1] + # get max radius of forgiveness + if isinstance(self[0], list): # [(x, y), r] case + r = max(self[1], r) + x1, y1 = self[0] + else: + x1, y1 = self - if ((user_answer[0] - corr_x) ** 2 + - (user_answer[1] - corr_y) ** 2) > r * r: + if isinstance(other[0], list): # [(x, y), r] case + r = max(other[1], r) + x2, y2 = other[0] + else: + x2, y2 = other + + if (x2 - x1) ** 2 + (y2 - y1) ** 2 > r * r: return False return True - if user_answer["use_targets"]: - is_equal = lambda user, correct: user == correct if ( - isinstance(user, unicode) and isinstance(correct, str)) else False - for draggable in user_answer['draggables']: - user_img_location = draggable.values()[0] - corr_img_location = correct_answer.get(draggable.keys()[0], None) - if not corr_img_location: - return False - if not is_equal(user_img_location, corr_img_location): +class DragAndDrop(object): + + def __init__(self): + self.correct_groups = OrderedDict() # groups + self.correct_positions = OrderedDict() # positions of comparing + self.user_groups = OrderedDict() + self.user_positions = OrderedDict() + + def grade(self): + ''' + Grade drag and drop problem. + If use_targets is True - checks if image placed on proper target. + If use_targets is False - checks if image placed on proper coordinates, + with setted radius of forgiveness (default is 10) + + Args: + user_input, correct_answer: json. Format: + + user_input: see module docstring + + correct_answer: + if use_targets is True: + {'1': 't1', 'name_with_icon': 't2'} + else: + {'1': '[10, 10]', 'name_with_icon': '[[10, 10], 20]'} + + Returns: + True or False. + ''' + if sorted(self.correct_groups.keys()) != sorted(self.user_groups.keys()): return False - return True + for groupname, draggable_ids in self.correct_groups.items(): + if sorted(draggable_ids) != sorted(self.user_groups[groupname]): + return False + + # from now self.groups and self.user_groups are equal + assert self.correct_groups == self.user_groups + + # Check fo every group that positions of every group element are equal + # with positions + + # 'denied' rule + # import ipdb; ipdb.set_trace() + denied_positions = [self.correct_positions[g].get('denied', []) + for g in self.correct_groups.keys()] + all_user_positions = [self.user_positions[g]['user'] + for g in self.correct_groups.keys()] + if not self.compare_positions(denied_positions, + all_user_positions, flag='denied'): + return False + + no_exact, no_allowed = False, False + # 'exact' rule + for groupname in self.correct_groups: + if self.correct_positions[groupname].get('exact', []): + if not self.compare_positions( + self.correct_positions[groupname]['exact'], + self.user_positions[groupname]['user'], flag='exact'): + return False + else: + no_exact = True + + # 'allowed' rule + for groupname in self.correct_groups: + if self.correct_positions[groupname].get('allowed', []): + if not self.compare_positions( + self.correct_positions[groupname]['allowed'], + self.user_positions[groupname]['user'], flag='allowed'): + return False + else: + no_allowed = True + + if no_allowed and no_exact: + return False + + return True + + def compare_positions(self, list1, list2, flag): + # import ipdb; ipdb.set_trace() + if flag == 'denied': + for el1 in list1: + for el2 in list2: + if PositionsCompare(el1) == PositionsCompare(el2): + return False + + if flag == 'allowed': + for el1, el2 in zip(sorted(list1), sorted(list2)): + if PositionsCompare(el1) != PositionsCompare(el2): + return False + + if flag == 'exact': + for el1, el2 in zip(list1, list2): + if PositionsCompare(el1) != PositionsCompare(el2): + return False + + return True + + def populate(self, correct_answer, user_answer): + """ """ + + if isinstance(correct_answer, dict): + for key, value in correct_answer.items(): + self.correct_groups[key] = [key] + self.correct_positions[key] = {'exact': [value]} + + user_answer = json.loads(user_answer) + self.use_targets = user_answer.get('use_targets') + + # create identical data structures + # user groups must mirror correct_groups + # and positions must reflect order in groups + + for groupname in self.correct_groups: + self.user_groups[groupname] = [] + self.user_positions[groupname] = {'user': []} + for draggable_dict in user_answer['draggables']: + # draggable_dict is 1-to-1 {draggable_name: position} + draggable_name = draggable_dict.keys()[0] + if draggable_name in self.correct_groups[groupname]: + self.user_groups[groupname].append(draggable_name) + self.user_positions[groupname]['user'].append( + draggable_dict[draggable_name]) + + # import ipdb; ipdb.set_trace() + + +def grade(user_input, correct_answer): + """ Support 2 interfaces""" + if isinstance(correct_answer, dict): + dnd = DragAndDrop() + + dnd.populate(correct_answer=correct_answer, user_answer=user_input) + return dnd.grade()