def setMark(self, grade, entered_by, details=True): """ Set the mark of the group members """ super(GroupActivityMark, self).setMark(grade) #assign mark for each member in the group group_members = GroupMember.objects.filter( group=self.group, activity=self.numeric_activity, confirmed=True) entered_by = get_entry_person(entered_by) for g_member in group_members: try: ngrade = NumericGrade.objects.get( activity=self.numeric_activity, member=g_member.student) except NumericGrade.DoesNotExist: ngrade = NumericGrade(activity=self.numeric_activity, member=g_member.student) ngrade.value = grade or decimal.Decimal(0) if grade is None: ngrade.flag = 'NOGR' else: ngrade.flag = 'GRAD' if details: ngrade.save(entered_by=entered_by, mark=self, group=self.group) else: # this is just a placeholder for a number-only mark ngrade.save(entered_by=entered_by, mark=None, group=self.group)
def test_mark_history(self): c = CourseOffering.objects.get(slug = self.c_slug) #add a numeric activity a = NumericActivity(offering = c, name = 'test_assignment_1', \ short_name = 'ta1', status = 'RLS', \ due_date = datetime.now(), max_grade = 100, position = 0) a.save() #take 2 students to make a group stud1 = Member.objects.get(person = Person.objects.get(userid = '0aaa0'), offering = c) stud2 = Member.objects.get(person = Person.objects.get(userid = '0aaa1'), offering = c) group = Group.objects.create(courseoffering = c, name = 'hello', manager = stud1) member1 = GroupMember.objects.create(group = group, student = stud1, confirmed = True, activity=a) member2 = GroupMember.objects.create(group = group, student = stud2, confirmed = True, activity=a) ngrade = NumericGrade(activity = a, member = stud2) ngrade.save(entered_by='ggbaker') #assign mark to 0aaa1 individually twice and via the group twice, make some interval between saves std_mark = StudentActivityMark(numeric_grade = ngrade, created_by = 'ggbaker') std_mark.setMark(20, entered_by='ggbaker') std_mark.save() group_mark = GroupActivityMark(group = group, numeric_activity = a, created_by = 'ggbaker') group_mark.setMark(30, entered_by='ggbaker') group_mark.save() std_mark = StudentActivityMark(numeric_grade = ngrade, created_by = 'ggbaker') std_mark.setMark(40, entered_by='ggbaker') std_mark.save() group_mark = GroupActivityMark(group = group, numeric_activity = a, created_by = 'ggbaker') group_mark.setMark(50, entered_by='ggbaker') group_mark.save() self.client.login_user('ggbaker') response = self.client.get(reverse('offering:marking:mark_history_student', args=(self.c_slug, a.slug, '0aaa1'))) self.assertEqual(response.status_code, 200) latest_act_mark = response.context['current_mark'] self.assertEqual(len(response.context['marks_individual']), 2) self.assertEqual(len(response.context['marks_via_group']), 2) self.assertEqual(group_mark, latest_act_mark)
def setMark(self, grade, entered_by, details=True): """ Set the mark of the group members """ super(GroupActivityMark, self).setMark(grade) #assign mark for each member in the group group_members = GroupMember.objects.filter(group=self.group, activity=self.numeric_activity, confirmed=True) entered_by = get_entry_person(entered_by) for g_member in group_members: try: ngrade = NumericGrade.objects.get(activity=self.numeric_activity, member=g_member.student) except NumericGrade.DoesNotExist: ngrade = NumericGrade(activity=self.numeric_activity, member=g_member.student) ngrade.value = grade ngrade.flag = 'GRAD' if details: ngrade.save(entered_by=entered_by, mark=self, group=self.group) else: # this is just a placeholder for a number-only mark ngrade.save(entered_by=entered_by, mark=None, group=self.group)
def test_api_permissions(self): """ Make sure the API views display activity/grade info at the right moments """ client = Client() client.login_user("0aaa0") grades_url = reverse('api.OfferingGrades', kwargs={'course_slug': TEST_COURSE_SLUG}) stats_url = reverse('api.OfferingStats', kwargs={'course_slug': TEST_COURSE_SLUG}) o = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) na = NumericActivity.objects.get(slug='a1') la = LetterActivity.objects.get(slug='rep') instr = Member.objects.get(person__userid='ggbaker', offering=o, role='INST') student = Member.objects.get(person__userid='0aaa0', offering=o, role='STUD') # mock out the cache so we get fresh results for each request with self.settings(CACHES={ 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache' } }): # below uses assertFalse to test for None, '', [], {}, all of which are fine. Just no real grade info. # invisible activities shouldn't appear na.status = 'INVI' na.save(entered_by=instr.person) la.status = 'INVI' la.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content) self.assertIsNone(self._get_by_slug(data, 'a1')) self.assertIsNone(self._get_by_slug(data, 'rep')) # no grades: shouldn't see na.status = 'URLS' na.save(entered_by=instr.person) la.status = 'URLS' la.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content) self.assertFalse(self._get_by_slug(data, 'a1')['grade']) self.assertFalse(self._get_by_slug(data, 'rep')['grade']) self.assertFalse(self._get_by_slug(data, 'a1')['details']) self.assertFalse(self._get_by_slug(data, 'rep')['details']) resp = client.get(stats_url) data = json.loads(resp.content) self.assertIn('unreleased activities', self._get_by_slug(data, 'a1')['missing_reason']) self.assertIn('unreleased activities', self._get_by_slug(data, 'rep')['missing_reason']) self.assertIsNone(self._get_by_slug(data, 'a1')['count']) self.assertIsNone(self._get_by_slug(data, 'rep')['count']) # grades but unreleased: shouldn't see ng = NumericGrade(activity=na, member=student, value=1, flag='GRAD', comment='Foo') ng.save(entered_by=instr.person) from marking.models import StudentActivityMark, ActivityComponentMark, ActivityComponent am = StudentActivityMark(numeric_grade=ng, mark=2, created_by='ggbaker', overall_comment='thecomment') am.save() comp = ActivityComponent.objects.filter(numeric_activity=na)[0] cm = ActivityComponentMark(activity_mark=am, value=2, comment='foo', activity_component=comp) cm.save() am.setMark(2, entered_by=instr.person) lg = LetterGrade(activity=la, member=student, letter_grade='A', flag='GRAD', comment='Foo') lg.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content) self.assertFalse(self._get_by_slug(data, 'a1')['grade']) self.assertFalse(self._get_by_slug(data, 'rep')['grade']) self.assertFalse(self._get_by_slug(data, 'a1')['details']) self.assertFalse(self._get_by_slug(data, 'rep')['details']) # release and they should appear na.status = 'RLS' na.save(entered_by=instr.person) la.status = 'RLS' la.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content) self.assertEqual(self._get_by_slug(data, 'a1')['grade'], '2') self.assertEqual(self._get_by_slug(data, 'rep')['grade'], 'A') self.assertEqual(self._get_by_slug(data, 'a1')['details']['overall_comment'], 'thecomment') self.assertIsNone(self._get_by_slug(data, 'rep')['details']) # letter grades have no marking resp = client.get(stats_url) data = json.loads(resp.content) self.assertIn('small classes', self._get_by_slug(data, 'a1')['missing_reason']) self.assertIn('small classes', self._get_by_slug(data, 'rep')['missing_reason'])
def test_get_grade(self): """ Make sure activity.get_grade() keeps the right things hidden """ o = CourseOffering.objects.get(slug=self.course_slug) na = NumericActivity.objects.get(slug='a1') la = LetterActivity.objects.get(slug='rep') instr = Person.objects.get(userid='ggbaker') student = Member.objects.get(person__userid='0aaa0', offering=o) na.status = 'RLS' na.save(entered_by=instr) la.status = 'RLS' la.save(entered_by=instr) # no grades yet self.assertEqual(na.get_grade(student.person, 'STUD'), None) self.assertEqual(la.get_grade(student.person, 'STUD'), None) self.assertEqual(na.get_grade(student.person, 'INST'), None) self.assertEqual(la.get_grade(student.person, 'INST'), None) # grades should be visible ng = NumericGrade(activity=na, member=student, value=1, flag='GRAD', comment='Foo') ng.save(entered_by=instr) lg = LetterGrade(activity=la, member=student, letter_grade='A', flag='GRAD', comment='Foo') lg.save(entered_by=instr) self.assertEqual(na.get_grade(student.person, 'STUD').grade, 1) self.assertEqual(la.get_grade(student.person, 'STUD').grade, 'A') self.assertEqual(na.get_grade(student.person, 'INST').grade, 1) self.assertEqual(la.get_grade(student.person, 'INST').grade, 'A') # unreleased: grades visible only to staff na.status = 'URLS' na.save(entered_by=instr) la.status = 'URLS' la.save(entered_by=instr) self.assertEqual(na.get_grade(student.person, 'STUD'), None) self.assertEqual(la.get_grade(student.person, 'STUD'), None) self.assertEqual(na.get_grade(student.person, 'INST').grade, 1) self.assertEqual(la.get_grade(student.person, 'INST').grade, 'A') # student shouldn't ever see invisible grade na.status = 'INVI' na.save(entered_by=instr) la.status = 'INVI' la.save(entered_by=instr) with self.assertRaises(RuntimeError): na.get_grade(student.person, 'STUD') with self.assertRaises(RuntimeError): la.get_grade(student.person, 'STUD') self.assertEqual(na.get_grade(student.person, 'INST').grade, 1) self.assertEqual(la.get_grade(student.person, 'INST').grade, 'A') # flag==NOGR handling ng.flag = 'NOGR' ng.save(entered_by=instr) lg.flag = 'NOGR' lg.save(entered_by=instr) na.status = 'RLS' na.save(entered_by=instr) la.status = 'RLS' la.save(entered_by=instr) self.assertEqual(na.get_grade(student.person, 'STUD'), None) self.assertEqual(la.get_grade(student.person, 'STUD'), None) self.assertEqual(na.get_grade(student.person, 'INST'), None) self.assertEqual(la.get_grade(student.person, 'INST'), None)
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 test_group_change(self): """ Test changing group <-> individual on an activity. Should only be possible in some conditions. """ s, c = create_offering() # add some assignments and members due = datetime.datetime.now() + datetime.timedelta(days=1) due_date = str(due.date()) due_time = due.time().strftime("%H:%M:%S") a = NumericActivity(name="Assignment 1", short_name="A1", status="RLS", offering=c, position=2, max_grade=15, percent=10, due_date=due, group=False) a.save() p = Person.objects.get(userid="ggbaker") m = Member(person=p, offering=c, role="INST", added_reason="UNK") m.save() p = Person.objects.get(userid="0aaa0") m = Member(person=p, offering=c, role="STUD", added_reason="UNK") m.save() client = Client() client.login_user("ggbaker") url = reverse('grades.views.edit_activity', kwargs={'course_slug': c.slug, 'activity_slug': a.slug}) # for whatever reason, '0' is group and '1' is individual for the group value submit_dict = {'name': a.name, 'short_name': a.short_name, 'status': a.status, 'due_date_0': due_date, 'due_date_1': due_time, 'percent': a.percent, 'max_grade': a.max_grade, 'group': '1', 'extend_group': 'None'} # no change response = client.post(url, submit_dict) self.assertEquals(response.status_code, 302) # successful submit -> redirect self.assertEquals(NumericActivity.objects.get(id=a.id).group, False) # change indiv -> group submit_dict['group'] = '0' response = client.post(url, submit_dict) self.assertEquals(response.status_code, 302) self.assertEquals(NumericActivity.objects.get(id=a.id).group, True) # try with activity past due a.due_date = datetime.datetime.now() - datetime.timedelta(days=1) a.save() submit_dict['due_date_0'] = str(a.due_date.date()) submit_dict['group'] = '0' response = client.post(url, submit_dict) self.assertEquals(response.status_code, 200) # error on form -> 200 and back to form with error self.assertContains(response, "due date has passed") # try with a mark in the system a.due_date = datetime.datetime.now() + datetime.timedelta(days=1) a.save() submit_dict['due_date_0'] = str(a.due_date.date()) submit_dict['group'] = '0' g = NumericGrade(activity=a, member=m, value=2, flag="GRAD") g.save(entered_by='ggbaker') response = client.post(url, submit_dict) self.assertEquals(response.status_code, 200) self.assertContains(response, "grades have already been given") # try with a submission in the system g.flag = "NOGR" g.save(entered_by='ggbaker') s = StudentSubmission(activity=a, member=m) s.save() response = client.post(url, submit_dict) self.assertEquals(response.status_code, 200) self.assertContains(response, "submissions have already been made")
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 automark_all(self, user: User) -> int: """ Fill in marking for any QuestionVersions that support it. Return number marked. """ versions = QuestionVersion.objects.filter( question__quiz=self, question__status='V').select_related('question') activity_components = self.activitycomponents_by_question() member_component_results = [ ] # : List[Tuple[Member, ActivityComponentMark]] for v in versions: member_component_results.extend( v.automark_all(activity_components=activity_components)) # Now the ugly work: combine the just-automarked components with any existing manual marking, and save... old_sam_lookup = { # dict to find old StudentActivityMarks sam.numeric_grade.member: sam for sam in StudentActivityMark.objects.filter( activity=self.activity).order_by('created_at').select_related( 'numeric_grade__member').prefetch_related( 'activitycomponentmark_set') } # dict to find old ActivityComponentMarks old_acm_by_component_id = defaultdict( dict) # : Dict[int, Dict[Member, ActivityComponentMark]] old_sam = StudentActivityMark.objects.filter(activity=self.activity).order_by('created_at') \ .select_related('numeric_grade__member').prefetch_related('activitycomponentmark_set') for sam in old_sam: for acm in sam.activitycomponentmark_set.all(): old_acm_by_component_id[acm.activity_component_id][ sam.numeric_grade.member] = acm numeric_grade_lookup = { # dict to find existing NumericGrades ng.member: ng for ng in NumericGrade.objects.filter( activity=self.activity).select_related('member') } all_components = set( ActivityComponent.objects.filter( numeric_activity_id=self.activity_id, deleted=False)) member_component_results.sort( key=lambda pair: pair[0].id) # ... get Members grouped together n_marked = 0 for member, member_acms in itertools.groupby(member_component_results, lambda pair: pair[0]): # Get a NumericGrade to work with try: ngrade = numeric_grade_lookup[member] except KeyError: ngrade = NumericGrade(activity_id=self.activity_id, member=member, flag='NOGR') ngrade.save(newsitem=False, entered_by=None, is_temporary=True) # Create ActivityMark to save under am = StudentActivityMark(numeric_grade=ngrade, activity_id=self.activity_id, created_by=user.username) old_am = old_sam_lookup.get(member) if old_am: am.overall_comment = old_am.overall_comment am.late_penalty = old_am.late_penalty am.mark_adjustment = old_am.mark_adjustment am.mark_adjustment_reason = old_am.mark_adjustment_reason am.save() # Find/create ActivityComponentMarks for each component auto_acm_lookup = { acm.activity_component: acm for _, acm in member_acms } any_missing = False acms = [] for c in all_components: # For each ActivityComponent, find one of # (1) just-auto-marked ActivityComponentMark, # (2) ActivityComponentMark from previous manual marking, # (3) nothing. if c in auto_acm_lookup: # (1) acm = auto_acm_lookup[c] acm.activity_mark = am n_marked += 1 elif c.id in old_acm_by_component_id and member in old_acm_by_component_id[ c.id]: # (2) old_acm = old_acm_by_component_id[c.id][member] acm = ActivityComponentMark(activity_mark=am, activity_component=c, value=old_acm.value, comment=old_acm.comment) else: # (3) acm = ActivityComponentMark(activity_mark=am, activity_component=c, value=None, comment=None) any_missing = True acm.save() acms.append(acm) if not any_missing: total = am.calculated_mark(acms) ngrade.value = total ngrade.flag = 'GRAD' am.mark = total else: ngrade.value = 0 ngrade.flag = 'NOGR' am.mark = None ngrade.save(newsitem=False, entered_by=user.username) am.save() return n_marked
def activity_marks_from_JSON(activity, userid, data, save=False): """ Build ActivityMark and ActivityComponentMark objects from imported JSON data. Since validating the input involves almost all of the work of saving the data, this function handles both. It is called once from is_valid with save==False to check everything, and again with save==True to actually do the work. Redundant yes, but it lets is_valid actually do its job without side effects. """ if not isinstance(data, dict): raise ValidationError('Outer JSON data structure must be an object.') if 'marks' not in data: raise ValidationError('Outer JSON data object must contain key "marks".') if not isinstance(data['marks'], list): raise ValidationError('Value for "marks" must be a list.') # All the ActivityMark and ActivityComponentMark objects get built here: # we basically have to do this work to validate anyway. components = ActivityComponent.objects.filter(numeric_activity_id=activity.id, deleted=False) components = dict((ac.slug, ac) for ac in components) found = set() not_found = set() combine = False # are we combining these marks with existing (as opposed to overwriting)? if 'combine' in data and bool(data['combine']): combine = True for markdata in data['marks']: if not isinstance(markdata, dict): raise ValidationError('Elements of array must be JSON objects.') # build the ActivityMark object and populate as much as possible for now. if activity.group and 'group' in markdata: # GroupActivityMark try: group = Group.objects.get(slug=markdata['group'], courseoffering=activity.offering) except Group.DoesNotExist: not_found.add(markdata['group']) continue am = GroupActivityMark(activity_id=activity.id, numeric_activity_id=activity.id, group=group, created_by=userid) recordid = markdata['group'] elif 'userid' in markdata: # StudentActivityMark try: member = Member.objects.get(person__userid=markdata['userid'], offering=activity.offering, role="STUD") except Member.DoesNotExist: not_found.add(markdata['userid']) continue am = StudentActivityMark(activity_id=activity.id, created_by=userid) recordid = markdata['userid'] else: raise ValidationError('Must specify "userid" or "group" for mark.') # check for duplicates in import if recordid in found: raise ValidationError('Duplicate marks for "%s".' % (recordid)) found.add(recordid) if combine: # if we're being asked to combine with old marks, get the old one (if exists) try: if activity.group: old_am = get_group_mark(activity, group) else: old_am = get_activity_mark_for_student(activity, member) except NumericGrade.DoesNotExist: old_am = None acms = [] # ActivityComponentMarks we will create for am # build ActivityComponentMarks found_comp_slugs = set() mark_total = 0 late_percent = decimal.Decimal(0) mark_penalty = decimal.Decimal(0) mark_penalty_reason = "" overall_comment = "" file_filename = None file_data = None file_mediatype = None # Added for the special case where we have a numeric mark only, without components. This can happen when # using the "mark for all groups/users" form. the_mark = decimal.Decimal(0) if combine and old_am: late_percent = old_am.late_penalty mark_penalty = old_am.mark_adjustment mark_penalty_reason = old_am.mark_adjustment_reason overall_comment = old_am.overall_comment for slug in markdata: # handle special-case slugs (that don't represent MarkComponents) if slug in ['userid', 'group']: continue elif slug == 'the_mark': try: the_mark = decimal.Decimal(str(markdata[slug])) except decimal.InvalidOperation: pass continue elif slug=="late_percent": try: late_percent = decimal.Decimal(str(markdata[slug])) except decimal.InvalidOperation: raise ValidationError('Value for "late_percent" must be numeric in record for "%s".' % (recordid)) continue elif slug=="mark_penalty": try: mark_penalty = decimal.Decimal(str(markdata[slug])) except decimal.InvalidOperation: raise ValidationError('Value for "mark_penalty" must be numeric in record for "%s".' % (recordid)) continue elif slug=="mark_penalty_reason": mark_penalty_reason = str(markdata[slug]) continue elif slug=="overall_comment": overall_comment = str(markdata[slug]) continue elif slug=="attach_type": file_mediatype = str(markdata[slug]) continue elif slug=="attach_filename": file_filename = str(markdata[slug]) continue elif slug=="attach_data": try: file_data = base64.b64decode(markdata[slug]) except TypeError: raise ValidationError('Invalid base64 file data for "%s"' % (recordid)) continue # handle MarkComponents if slug in components and slug not in found_comp_slugs: comp = components[slug] found_comp_slugs.add(slug) elif slug in components: # shouldn't happen because JSON lib forces unique keys, but let's be extra safe... raise ValidationError('Multiple values given for "%s" in record for "%s".' % (slug, recordid)) else: raise ValidationError('Mark component "%s" not found in record for "%s".' % (slug, recordid)) cm = ActivityComponentMark(activity_component=comp) acms.append(cm) # can't set activity_mark yet since it doesn't have an id componentdata = markdata[slug] if not isinstance(componentdata, dict): raise ValidationError('Mark component data must be JSON object (in "%s" for "%s").' % (slug, recordid)) if 'mark' not in componentdata: raise ValidationError('Must give "mark" for "%s" in record for "%s".' % (comp.title, recordid)) try: value = decimal.Decimal(str(componentdata['mark'])) except decimal.InvalidOperation: raise ValidationError('Value for "mark" must be numeric for "%s" in record for "%s".' % (comp.title, recordid)) cm.value = value mark_total += float(componentdata['mark']) if 'comment' in componentdata and save: cm.comment = str(componentdata['comment']) if 'display_raw' in componentdata and save: cm.set_display_raw(bool(componentdata['display_raw'])) # In the case of combined gradings, we have to get the value from old components to add to it. if combine: for slug in set(components.keys()) - found_comp_slugs: # handle missing components cm = ActivityComponentMark(activity_component=components[slug]) acms.append(cm) # can't set activity_mark yet since it doesn't have an id if old_am: old_cm = ActivityComponentMark.objects.get(activity_mark=old_am, activity_component=components[slug]) if old_cm.value is not None: mark_total += float(old_cm.value) cm.value = old_cm.value cm.comment = old_cm.comment cm.set_display_raw(old_cm.display_raw()) # handle file attachment if file_filename or file_data or file_mediatype: # new attachment if not (file_filename and file_data and file_mediatype): raise ValidationError('Must specify all or none of "attach_type", "attach_filename", "attach_data" in record for "%s"' % (recordid)) am.file_mediatype = file_mediatype if save: am.file_attachment.save(name=file_filename, content=ContentFile(file_data), save=False) elif combine and old_am: # recycle old am.file_attachment = old_am.file_attachment am.file_mediatype = old_am.file_mediatype else: # none am.file_attachment = None am.file_mediatype = None am.late_penalty = late_percent am.mark_adjustment = mark_penalty am.mark_adjustment_reason = mark_penalty_reason am.overall_comment = overall_comment mark_total = the_mark or ((1-late_percent/decimal.Decimal(100)) * (decimal.Decimal(str(mark_total)) - mark_penalty)) # put the total mark and numeric grade objects in place am.mark = mark_total value = mark_total if isinstance(am, StudentActivityMark): grades = NumericGrade.objects.filter(activity_id=activity.id, member=member) if grades: numeric_grade = grades[0] numeric_grade.flag = "GRAD" else: numeric_grade = NumericGrade(activity_id=activity.id, member=member, flag="GRAD") numeric_grade.value = value if save: numeric_grade.save(entered_by=userid) am.numeric_grade = numeric_grade else: group_members = GroupMember.objects.filter(group=group, activity_id=activity.id, confirmed=True) for g_member in group_members: try: ngrade = NumericGrade.objects.get(activity_id=activity.id, member=g_member.student) except NumericGrade.DoesNotExist: ngrade = NumericGrade(activity_id=activity.id, member=g_member.student) ngrade.value = value ngrade.flag = 'GRAD' if save: ngrade.save(entered_by=userid) if save: am.save() for cm in acms: cm.activity_mark = am cm.save() return found, not_found
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_api_permissions(self): """ Make sure the API views display activity/grade info at the right moments """ client = Client() client.login_user("0aaa0") grades_url = reverse('api:OfferingGrades', kwargs={'course_slug': TEST_COURSE_SLUG}) stats_url = reverse('api:OfferingStats', kwargs={'course_slug': TEST_COURSE_SLUG}) o = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) na = NumericActivity.objects.get(slug='a1') la = LetterActivity.objects.get(slug='rep') instr = Member.objects.get(person__userid='ggbaker', offering=o, role='INST') student = Member.objects.get(person__userid='0aaa0', offering=o, role='STUD') # mock out the cache so we get fresh results for each request with self.settings(CACHES={ 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache' } }): # below uses assertFalse to test for None, '', [], {}, all of which are fine. Just no real grade info. # invisible activities shouldn't appear na.status = 'INVI' na.save(entered_by=instr.person) la.status = 'INVI' la.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content.decode('utf8')) self.assertIsNone(self._get_by_slug(data, 'a1')) self.assertIsNone(self._get_by_slug(data, 'rep')) # no grades: shouldn't see na.status = 'URLS' na.save(entered_by=instr.person) la.status = 'URLS' la.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content.decode('utf8')) self.assertFalse(self._get_by_slug(data, 'a1')['grade']) self.assertFalse(self._get_by_slug(data, 'rep')['grade']) self.assertFalse(self._get_by_slug(data, 'a1')['details']) self.assertFalse(self._get_by_slug(data, 'rep')['details']) resp = client.get(stats_url) data = json.loads(resp.content.decode('utf8')) self.assertIn('unreleased activities', self._get_by_slug(data, 'a1')['missing_reason']) self.assertIn('unreleased activities', self._get_by_slug(data, 'rep')['missing_reason']) self.assertIsNone(self._get_by_slug(data, 'a1')['count']) self.assertIsNone(self._get_by_slug(data, 'rep')['count']) # grades but unreleased: shouldn't see ng = NumericGrade(activity=na, member=student, value=1, flag='GRAD', comment='Foo') ng.save(entered_by=instr.person) from marking.models import StudentActivityMark, ActivityComponentMark, ActivityComponent am = StudentActivityMark(numeric_grade=ng, mark=2, created_by='ggbaker', overall_comment='thecomment') am.save() comp = ActivityComponent.objects.filter(numeric_activity=na)[0] cm = ActivityComponentMark(activity_mark=am, value=2, comment='foo', activity_component=comp) cm.save() am.setMark(2, entered_by=instr.person) lg = LetterGrade(activity=la, member=student, letter_grade='A', flag='GRAD', comment='Foo') lg.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content.decode('utf8')) self.assertFalse(self._get_by_slug(data, 'a1')['grade']) self.assertFalse(self._get_by_slug(data, 'rep')['grade']) self.assertFalse(self._get_by_slug(data, 'a1')['details']) self.assertFalse(self._get_by_slug(data, 'rep')['details']) # release and they should appear na.status = 'RLS' na.save(entered_by=instr.person) la.status = 'RLS' la.save(entered_by=instr.person) resp = client.get(grades_url) data = json.loads(resp.content.decode('utf8')) self.assertEqual(self._get_by_slug(data, 'a1')['grade'], '2.00') self.assertEqual(self._get_by_slug(data, 'rep')['grade'], 'A') self.assertEqual( self._get_by_slug(data, 'a1')['details']['overall_comment'], 'thecomment') self.assertIsNone(self._get_by_slug( data, 'rep')['details']) # letter grades have no marking resp = client.get(stats_url) data = json.loads(resp.content.decode('utf8')) self.assertIn('small classes', self._get_by_slug(data, 'a1')['missing_reason']) self.assertIn('small classes', self._get_by_slug(data, 'rep')['missing_reason'])
def test_get_grade(self): """ Make sure activity.get_grade() keeps the right things hidden """ o = CourseOffering.objects.get(slug=self.course_slug) na = NumericActivity.objects.get(slug='a1') la = LetterActivity.objects.get(slug='rep') instr = Person.objects.get(userid='ggbaker') student = Member.objects.get(person__userid='0aaa0', offering=o) na.status = 'RLS' na.save(entered_by=instr) la.status = 'RLS' la.save(entered_by=instr) # no grades yet self.assertEqual(na.get_grade(student.person, 'STUD'), None) self.assertEqual(la.get_grade(student.person, 'STUD'), None) self.assertEqual(na.get_grade(student.person, 'INST'), None) self.assertEqual(la.get_grade(student.person, 'INST'), None) # grades should be visible ng = NumericGrade(activity=na, member=student, value=1, flag='GRAD', comment='Foo') ng.save(entered_by=instr) lg = LetterGrade(activity=la, member=student, letter_grade='A', flag='GRAD', comment='Foo') lg.save(entered_by=instr) self.assertEqual(na.get_grade(student.person, 'STUD').grade, 1) self.assertEqual(la.get_grade(student.person, 'STUD').grade, 'A') self.assertEqual(na.get_grade(student.person, 'INST').grade, 1) self.assertEqual(la.get_grade(student.person, 'INST').grade, 'A') # unreleased: grades visible only to staff na.status = 'URLS' na.save(entered_by=instr) la.status = 'URLS' la.save(entered_by=instr) self.assertEqual(na.get_grade(student.person, 'STUD'), None) self.assertEqual(la.get_grade(student.person, 'STUD'), None) self.assertEqual(na.get_grade(student.person, 'INST').grade, 1) self.assertEqual(la.get_grade(student.person, 'INST').grade, 'A') # student shouldn't ever see invisible grade na.status = 'INVI' na.save(entered_by=instr) la.status = 'INVI' la.save(entered_by=instr) with self.assertRaises(RuntimeError): na.get_grade(student.person, 'STUD') with self.assertRaises(RuntimeError): la.get_grade(student.person, 'STUD') self.assertEqual(na.get_grade(student.person, 'INST').grade, 1) self.assertEqual(la.get_grade(student.person, 'INST').grade, 'A') # flag==NOGR handling ng.flag = 'NOGR' ng.save(entered_by=instr) lg.flag = 'NOGR' lg.save(entered_by=instr) na.status = 'RLS' na.save(entered_by=instr) la.status = 'RLS' la.save(entered_by=instr) self.assertEqual(na.get_grade(student.person, 'STUD'), None) self.assertEqual(la.get_grade(student.person, 'STUD'), None) self.assertEqual(na.get_grade(student.person, 'INST'), None) self.assertEqual(la.get_grade(student.person, 'INST'), None)
def test_group_change(self): """ Test changing group <-> individual on an activity. Should only be possible in some conditions. """ s, c = create_offering() # add some assignments and members due = datetime.datetime.now() + datetime.timedelta(days=1) due_date = str(due.date()) due_time = due.time().strftime("%H:%M:%S") a = NumericActivity(name="Assignment 1", short_name="A1", status="RLS", offering=c, position=2, max_grade=15, percent=10, due_date=due, group=False) a.save() p = Person.objects.get(userid="ggbaker") m = Member(person=p, offering=c, role="INST", added_reason="UNK") m.save() p = Person.objects.get(userid="0aaa0") m = Member(person=p, offering=c, role="STUD", added_reason="UNK") m.save() client = Client() client.login_user("ggbaker") url = reverse('offering:edit_activity', kwargs={ 'course_slug': c.slug, 'activity_slug': a.slug }) # for whatever reason, '0' is group and '1' is individual for the group value submit_dict = { 'name': a.name, 'short_name': a.short_name, 'status': a.status, 'due_date_0': due_date, 'due_date_1': due_time, 'percent': a.percent, 'max_grade': a.max_grade, 'group': '1', 'extend_group': 'None' } # no change response = client.post(url, submit_dict) self.assertEqual(response.status_code, 302) # successful submit -> redirect self.assertEqual(NumericActivity.objects.get(id=a.id).group, False) # change indiv -> group submit_dict['group'] = '0' response = client.post(url, submit_dict) self.assertEqual(response.status_code, 302) self.assertEqual(NumericActivity.objects.get(id=a.id).group, True) # try with activity past due a.due_date = datetime.datetime.now() - datetime.timedelta(days=1) a.save() submit_dict['due_date_0'] = str(a.due_date.date()) submit_dict['group'] = '0' response = client.post(url, submit_dict) self.assertEqual( response.status_code, 200) # error on form -> 200 and back to form with error self.assertContains(response, "due date has passed") # try with a mark in the system a.due_date = datetime.datetime.now() + datetime.timedelta(days=1) a.save() submit_dict['due_date_0'] = str(a.due_date.date()) submit_dict['group'] = '0' g = NumericGrade(activity=a, member=m, value=2, flag="GRAD") g.save(entered_by='ggbaker') response = client.post(url, submit_dict) self.assertEqual(response.status_code, 200) self.assertContains(response, "grades have already been given") # try with a submission in the system g.flag = "NOGR" g.save(entered_by='ggbaker') s = StudentSubmission(activity=a, member=m) s.save() response = client.post(url, submit_dict) self.assertEqual(response.status_code, 200) self.assertContains(response, "submissions have already been made")
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)