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 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_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'])