def parse_and_validate_formula(formula, course, activity, numeric_activities): """ Handy function to parse the formula and validate if the activity references in the formula are in the numeric_activities list Return the parsed formula if no exception raised May raise exception: ParseException, ValidateError """ for a in numeric_activities: if not isinstance(a, NumericActivity): raise TypeError(u'NumericActivity list is required') try: parsed_expr = parse(formula, course, activity) activities_dict = activities_dictionary(numeric_activities) cols = set([]) cols = cols_used(parsed_expr) for col in cols: if not col in activities_dict: raise ValidationError(u'Invalid activity reference') except ParseException: raise ValidationError(u'Incorrect formula syntax') return parsed_expr
def parse_and_validate_formula(formula, course, activity, numeric_activities): """ Handy function to parse the formula and validate if the activity references in the formula are in the numeric_activities list Return the parsed formula if no exception raised May raise exception: ParseException, ValidateError """ for a in numeric_activities: if not isinstance(a, NumericActivity): raise TypeError(u'NumericActivity list is required') try: parsed_expr = parse(formula, course, activity) activities_dict = activities_dictionary(numeric_activities) cols = set([]) cols = cols_used(parsed_expr) for col in cols: if not col in activities_dict: raise ValidationError(u'Invalid activity reference') except ParseException: raise ValidationError(u'Incorrect formula syntax') return parsed_expr
def test_formulas(self): """ Test the formula parsing & evaluation. """ # set up course and related data s, c = create_offering() p = Person.objects.get(userid="0aaa0") m = Member(person=p, offering=c, role="STUD", credits=3, added_reason="UNK") m.save() a = NumericActivity(name="Paragraph", short_name=u"\u00b6", status="RLS", offering=c, position=3, max_grade=40, percent=5) a.save() g = NumericGrade(activity=a, member=m, value="4.5", flag="CALC") g.save(entered_by='ggbaker') a1 = NumericActivity(name="Assignment #1", short_name="A1", status="RLS", offering=c, position=1, max_grade=15, percent=10) a1.save() g = NumericGrade(activity=a1, member=m, value=10, flag="GRAD") g.save(entered_by='ggbaker') a2 = NumericActivity(name="Assignment #2", short_name="A2", status="URLS", offering=c, position=2, max_grade=40, percent=20) a2.save(entered_by='ggbaker') g = NumericGrade(activity=a2, member=m, value=30, flag="GRAD") g.save(entered_by='ggbaker') ca = CalNumericActivity(name="Final Grade", short_name=u"FG", status="RLS", offering=c, position=4, max_grade=1) ca.save() activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) # make sure a formula can be pickled and unpickled safely (i.e. can be cached) tree = parse("sum([Assignment #1], [A1], [A2])/20*-3", c, ca) p = pickle.dumps(tree) tree2 = pickle.loads(p) self.assertEqual(tree, tree2) # check that it found the right list of columns used self.assertEqual(cols_used(tree), set(['A1', 'A2', 'Assignment #1'])) # test parsing and evaluation to make sure we get the right values out for expr, correct in test_formulas: tree = parse(expr, c, ca) res = eval_parse(tree, ca, act_dict, m, False) self.assertAlmostEqual(correct, res, msg=u"Incorrect result for %s"%(expr,)) # test some badly-formed stuff for appropriate exceptions tree = parse("1 + BEST(3, [A1], [A2])", c, ca) self.assertRaises(EvalException, eval_parse, tree, ca, act_dict, m, True) tree = parse("1 + BEST(0, [A1], [A2])", c, ca) self.assertRaises(EvalException, eval_parse, tree, ca, act_dict, m, True) tree = parse("[Foo] /2", c, ca) self.assertRaises(KeyError, eval_parse, tree, ca, act_dict, m, True) tree = parse("[a1] /2", c, ca) self.assertRaises(KeyError, eval_parse, tree, ca, act_dict, m, True) self.assertRaises(ParseException, parse, "AVG()", c, ca) self.assertRaises(ParseException, parse, "(2+3*84", c, ca) self.assertRaises(ParseException, parse, "2+3**84", c, ca) self.assertRaises(ParseException, parse, "AVG(2,3,4", c, ca) self.assertRaises(ParseException, parse, "{something}", c, ca) # test visible/invisible switching tree = parse("[Assignment #2]", c, ca) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) res = eval_parse(tree, ca, act_dict, m, False) self.assertAlmostEqual(res, 30.0) # test unreleased/missing grade conditions expr = "[Assignment #2]" tree = parse(expr, c, ca) # unreleased assignment (with grade) a2.status='URLS' a2.save() activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) # explicit no grade (released assignment) g.flag="NOGR" g.save(entered_by='ggbaker') a2.status='RLS' a2.save(entered_by='ggbaker') activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) # no grade in database (released assignment) g.delete() activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) # test [[activitytotal]] expr = "[[activitytotal]]" tree = parse(expr, c, ca) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 7.229166666)
def calculate_numeric_grade(course, activity, student=None): """ Calculate all the student's grade in the course's CalNumericActivity. If student param is specified, this student's grade is calculated instead of the whole class, please also make sure this student is in the course before passing the student param. """ if not isinstance(course, CourseOffering): raise TypeError('CourseOffering type is required') if not isinstance(activity, CalNumericActivity): raise TypeError('CalNumericActivity type is required') numeric_activities = NumericActivity.objects.filter(offering=course, deleted=False) act_dict = activities_dictionary(numeric_activities) try: parsed_expr = parse_and_validate_formula(activity.formula, activity.offering, activity, numeric_activities) except ValidationError as e: raise ValidationError('Formula Error: ' + e.args[0]) if student != None: # calculate for one student if not isinstance(student, Member): raise TypeError('Member type is required') student_list = [student] numeric_grade_list = NumericGrade.objects.filter(activity=activity, member=student) else: # calculate for all student student_list = Member.objects.filter(offering=course, role='STUD') numeric_grade_list = NumericGrade.objects.filter( activity=activity).select_related('member') ignored = 0 visible = activity.status == "RLS" for s in student_list: # calculate grade try: result = eval_parse(parsed_expr, activity, act_dict, s, visible) result = decimal.Decimal(str(result)) # convert to decimal except EvalException: raise EvalException( "Formula Error: Can not evaluate formula for student: '%s'" % s.person.name()) # save grade member_found = False for numeric_grade in numeric_grade_list: if numeric_grade.member == s: member_found = True if numeric_grade.flag != "CALC": # ignore manually-set grades ignored += 1 elif result != numeric_grade.value: # only save when the value changes numeric_grade.value = result numeric_grade.flag = "CALC" numeric_grade.save(newsitem=False, entered_by=None) break if not member_found: numeric_grade = NumericGrade(activity=activity, member=s, value=str(result), flag='CALC') numeric_grade.save(newsitem=False, entered_by=None) uses_unreleased = True in (act_dict[c].status != 'RLS' for c in cols_used(parsed_expr)) hiding_info = visible and uses_unreleased if student != None: return StudentActivityInfo(student, activity, FLAGS['CALC'], numeric_grade.value, None).display_grade_staff(), hiding_info else: return ignored, hiding_info
def calculate_numeric_grade(course, activity, student=None): """ Calculate all the student's grade in the course's CalNumericActivity. If student param is specified, this student's grade is calculated instead of the whole class, please also make sure this student is in the course before passing the student param. """ if not isinstance(course, CourseOffering): raise TypeError('CourseOffering type is required') if not isinstance(activity, CalNumericActivity): raise TypeError('CalNumericActivity type is required') numeric_activities = NumericActivity.objects.filter(offering=course, deleted=False) act_dict = activities_dictionary(numeric_activities) try: parsed_expr = parse_and_validate_formula(activity.formula, activity.offering, activity, numeric_activities) except ValidationError as e: raise ValidationError('Formula Error: ' + e.args[0]) if student != None: # calculate for one student if not isinstance(student, Member): raise TypeError('Member type is required') student_list = [student] numeric_grade_list = NumericGrade.objects.filter(activity=activity, member=student) else: # calculate for all student student_list = Member.objects.filter(offering=course, role='STUD') numeric_grade_list = NumericGrade.objects.filter(activity = activity).select_related('member') ignored = 0 visible = activity.status=="RLS" for s in student_list: # calculate grade try: result = eval_parse(parsed_expr, activity, act_dict, s, visible) result = decimal.Decimal(str(result)) # convert to decimal except EvalException: raise EvalException("Formula Error: Can not evaluate formula for student: '%s'" % s.person.name()) # save grade member_found = False for numeric_grade in numeric_grade_list: if numeric_grade.member == s: member_found = True if numeric_grade.flag != "CALC": # ignore manually-set grades ignored += 1 elif result != numeric_grade.value: # only save when the value changes numeric_grade.value = result numeric_grade.flag = "CALC" numeric_grade.save(newsitem=False, entered_by=None) break if not member_found: numeric_grade = NumericGrade(activity=activity, member=s, value=str(result), flag='CALC') numeric_grade.save(newsitem=False, entered_by=None) uses_unreleased = True in (act_dict[c].status != 'RLS' for c in cols_used(parsed_expr)) hiding_info = visible and uses_unreleased if student != None: return StudentActivityInfo(student, activity, FLAGS['CALC'], numeric_grade.value, None).display_grade_staff(), hiding_info else: return ignored, hiding_info
def test_formulas(self): """ Test the formula parsing & evaluation. """ # set up course and related data s, c = create_offering() p = Person.objects.get(userid="0aaa0") m = Member(person=p, offering=c, role="STUD", credits=3, added_reason="UNK") m.save() a = NumericActivity(name="Paragraph", short_name="\u00b6", status="RLS", offering=c, position=3, max_grade=40, percent=5) a.save() g = NumericGrade(activity=a, member=m, value="4.5", flag="CALC") g.save(entered_by='ggbaker') a1 = NumericActivity(name="Assignment #1", short_name="A1", status="RLS", offering=c, position=1, max_grade=15, percent=10) a1.save() g = NumericGrade(activity=a1, member=m, value=10, flag="GRAD") g.save(entered_by='ggbaker') a2 = NumericActivity(name="Assignment #2", short_name="A2", status="URLS", offering=c, position=2, max_grade=40, percent=20) a2.save(entered_by='ggbaker') g = NumericGrade(activity=a2, member=m, value=30, flag="GRAD") g.save(entered_by='ggbaker') ca = CalNumericActivity(name="Final Grade", short_name="FG", status="RLS", offering=c, position=4, max_grade=1) ca.save() activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) # make sure a formula can be pickled and unpickled safely (i.e. can be cached) tree = parse("sum([Assignment #1], [A1], [A2])/20*-3", c, ca) p = pickle.dumps(tree) tree2 = pickle.loads(p) self.assertEqual(tree, tree2) # check that it found the right list of columns used self.assertEqual(cols_used(tree), set(['A1', 'A2', 'Assignment #1'])) # test parsing and evaluation to make sure we get the right values out for expr, correct in test_formulas: tree = parse(expr, c, ca) res = eval_parse(tree, ca, act_dict, m, False) self.assertAlmostEqual(correct, res, msg="Incorrect result for %s" % (expr, )) # test some badly-formed stuff for appropriate exceptions tree = parse("1 + BEST(3, [A1], [A2])", c, ca) self.assertRaises(EvalException, eval_parse, tree, ca, act_dict, m, True) tree = parse("1 + BEST(0, [A1], [A2])", c, ca) self.assertRaises(EvalException, eval_parse, tree, ca, act_dict, m, True) tree = parse("[Foo] /2", c, ca) self.assertRaises(KeyError, eval_parse, tree, ca, act_dict, m, True) tree = parse("[a1] /2", c, ca) self.assertRaises(KeyError, eval_parse, tree, ca, act_dict, m, True) self.assertRaises(ParseException, parse, "AVG()", c, ca) self.assertRaises(ParseException, parse, "(2+3*84", c, ca) self.assertRaises(ParseException, parse, "2+3**84", c, ca) self.assertRaises(ParseException, parse, "AVG(2,3,4", c, ca) self.assertRaises(ParseException, parse, "{something}", c, ca) # test visible/invisible switching tree = parse("[Assignment #2]", c, ca) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) res = eval_parse(tree, ca, act_dict, m, False) self.assertAlmostEqual(res, 30.0) # test unreleased/missing grade conditions expr = "[Assignment #2]" tree = parse(expr, c, ca) # unreleased assignment (with grade) should not be included in the calculation a2.status = 'URLS' a2.save() activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) # ... unless the instructor said to do so. ca.set_calculation_leak(True) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 30.0) # explicit no grade (released assignment) g.flag = "NOGR" g.save(entered_by='ggbaker') a2.status = 'RLS' a2.save(entered_by='ggbaker') activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) # no grade in database (released assignment) g.delete() activities = NumericActivity.objects.filter(offering=c) act_dict = activities_dictionary(activities) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 0.0) # test [[activitytotal]] expr = "[[activitytotal]]" tree = parse(expr, c, ca) res = eval_parse(tree, ca, act_dict, m, True) self.assertAlmostEqual(res, 7.229166666)