Exemple #1
0
 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)
Exemple #2
0
    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)
Exemple #3
0
 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)
Exemple #4
0
    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)
Exemple #5
0
    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")
Exemple #6
0
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
Exemple #7
0
    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
Exemple #8
0
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
Exemple #9
0
    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)
Exemple #10
0
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
Exemple #11
0
def activity_marks_from_JSON(activity, userid, data):
    """
    Build ActivityMark and ActivityComponentMark objects from imported JSON data.
    
    Return three lists: all ActivityMarks and all ActivityComponentMark and all NumericGrades *all not yet saved*.
    """
    if not isinstance(data, dict):
        raise ValidationError(u'Outer JSON data structure must be an object.')
    if 'marks' not in data:
        raise ValidationError(
            u'Outer JSON data object must contain key "marks".')
    if not isinstance(data['marks'], list):
        raise ValidationError(u'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=activity,
                                                  deleted=False)
    components = dict((ac.slug, ac) for ac in components)
    activity_marks = []
    activity_component_marks = []
    numeric_grades = []
    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(u'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:
                raise ValidationError(u'Group with id "%s" not found.' %
                                      (markdata['group']))
            am = GroupActivityMark(activity=activity,
                                   numeric_activity=activity,
                                   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:
                raise ValidationError(u'Userid %s not in course.' %
                                      (markdata['userid']))
            am = StudentActivityMark(activity=activity, created_by=userid)
            recordid = markdata['userid']
        else:
            raise ValidationError(
                u'Must specify "userid" or "group" for mark.')

        # check for duplicates in import
        if recordid in found:
            raise ValidationError(u'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

        activity_marks.append(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

        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 == "late_percent":
                try:
                    late_percent = decimal.Decimal(str(markdata[slug]))
                except decimal.InvalidOperation:
                    raise ValidationError(
                        u'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(
                        u'Value for "mark_penalty" must be numeric in record for "%s".'
                        % (recordid))
                continue
            elif slug == "mark_penalty_reason":
                mark_penalty_reason = unicode(markdata[slug])
                continue
            elif slug == "overall_comment":
                overall_comment = unicode(markdata[slug])
                continue
            elif slug == "attach_type":
                file_mediatype = str(markdata[slug])
                continue
            elif slug == "attach_filename":
                file_filename = unicode(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 happend because JSON lib forces unique keys, but let's be extra safe...
                raise ValidationError(
                    u'Multiple values given for "%s" in record for "%s".' %
                    (slug, recordid))
            else:
                raise ValidationError(
                    u'Mark component "%s" not found in record for "%s".' %
                    (slug, recordid))

            cm = ActivityComponentMark(activity_mark=am,
                                       activity_component=comp)
            activity_component_marks.append(cm)

            componentdata = markdata[slug]
            if not isinstance(componentdata, dict):
                raise ValidationError(
                    u'Mark component data must be JSON object (in "%s" for "%s").'
                    % (slug, recordid))

            if 'mark' not in componentdata:
                raise ValidationError(
                    u'Must give "mark" for "%s" in record for "%s".' %
                    (comp.title, recordid))

            try:
                value = decimal.Decimal(str(componentdata['mark']))
            except decimal.InvalidOperation:
                raise ValidationError(
                    u'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:
                cm.comment = unicode(componentdata['comment'])

        for slug in set(components.keys()) - found_comp_slugs:
            # handle missing components
            cm = ActivityComponentMark(activity_mark=am,
                                       activity_component=components[slug])
            activity_component_marks.append(cm)
            if combine and old_am:
                old_cm = ActivityComponentMark.objects.get(
                    activity_mark=old_am, activity_component=components[slug])
                cm.value = old_cm.value
                cm.comment = old_cm.comment
                mark_total += float(cm.value)
            else:
                cm.value = decimal.Decimal(0)
                cm.comment = ''

        # 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(
                    u'Must specify all or none of "attach_type", "attach_filename", "attach_data" in record for "%s"'
                    % (recordid))
            am.file_attachment.save(name=file_filename,
                                    content=ContentFile(file_data),
                                    save=False)
            am.file_mediatype = file_mediatype
        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 = (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=activity,
                                                 member=member)
            if grades:
                numeric_grade = grades[0]
                numeric_grade.flag = "GRAD"
            else:
                numeric_grade = NumericGrade(activity=activity,
                                             member=member,
                                             flag="GRAD")

            numeric_grade.value = value
            am.numeric_grade = numeric_grade
            numeric_grades.append(numeric_grade)

        else:
            group_members = GroupMember.objects.filter(group=group,
                                                       activity=activity,
                                                       confirmed=True)
            for g_member in group_members:
                try:
                    ngrade = NumericGrade.objects.get(activity=activity,
                                                      member=g_member.student)
                except NumericGrade.DoesNotExist:
                    ngrade = NumericGrade(activity=activity,
                                          member=g_member.student)
                ngrade.value = value
                ngrade.flag = 'GRAD'
                numeric_grades.append(ngrade)

    return (activity_marks, activity_component_marks, numeric_grades)
Exemple #12
0
    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'])
Exemple #13
0
    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)
Exemple #14
0
    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")
Exemple #15
0
    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)
Exemple #16
0
    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'])
Exemple #17
0
def activity_marks_from_JSON(activity, userid, data):
    """
    Build ActivityMark and ActivityComponentMark objects from imported JSON data.
    
    Return three lists: all ActivityMarks and all ActivityComponentMark and all NumericGrades *all not yet saved*.
    """
    if not isinstance(data, dict):
        raise ValidationError(u'Outer JSON data structure must be an object.')
    if 'marks' not in data:
        raise ValidationError(u'Outer JSON data object must contain key "marks".')
    if not isinstance(data['marks'], list):
        raise ValidationError(u'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=activity, deleted=False)
    components = dict((ac.slug, ac) for ac in components)
    activity_marks = []
    activity_component_marks = []
    numeric_grades = []
    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(u'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:
                raise ValidationError(u'Group with id "%s" not found.' % (markdata['group']))
            am = GroupActivityMark(activity=activity, numeric_activity=activity, 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:
                raise ValidationError(u'Userid %s not in course.' % (markdata['userid']))
            am = StudentActivityMark(activity=activity, created_by=userid)
            recordid = markdata['userid']
        else:
            raise ValidationError(u'Must specify "userid" or "group" for mark.')

        # check for duplicates in import
        if recordid in found:
            raise ValidationError(u'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

        activity_marks.append(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

        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=="late_percent":
                try:
                    late_percent = decimal.Decimal(str(markdata[slug]))
                except decimal.InvalidOperation:
                    raise ValidationError(u'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(u'Value for "mark_penalty" must be numeric in record for "%s".' % (recordid))
                continue
            elif slug=="mark_penalty_reason":
                mark_penalty_reason = unicode(markdata[slug])
                continue
            elif slug=="overall_comment":
                overall_comment = unicode(markdata[slug])
                continue
            elif slug=="attach_type":
                file_mediatype = str(markdata[slug])
                continue
            elif slug=="attach_filename":
                file_filename = unicode(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 happend because JSON lib forces unique keys, but let's be extra safe...
                raise ValidationError(u'Multiple values given for "%s" in record for "%s".' % (slug, recordid))
            else:
                raise ValidationError(u'Mark component "%s" not found in record for "%s".' % (slug, recordid))

            cm = ActivityComponentMark(activity_mark=am, activity_component=comp)
            activity_component_marks.append(cm)

            componentdata = markdata[slug]
            if not isinstance(componentdata, dict):
                raise ValidationError(u'Mark component data must be JSON object (in "%s" for "%s").' % (slug, recordid))

            if 'mark' not in componentdata:
                raise ValidationError(u'Must give "mark" for "%s" in record for "%s".' % (comp.title, recordid))
            
            try:
                value = decimal.Decimal(str(componentdata['mark']))
            except decimal.InvalidOperation:
                raise ValidationError(u'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:
                cm.comment = unicode(componentdata['comment'])

        for slug in set(components.keys()) - found_comp_slugs:
            # handle missing components
            cm = ActivityComponentMark(activity_mark=am, activity_component=components[slug])
            activity_component_marks.append(cm)
            if combine and old_am:
                old_cm = ActivityComponentMark.objects.get(activity_mark=old_am, activity_component=components[slug])
                cm.value = old_cm.value
                cm.comment = old_cm.comment
                mark_total += float(cm.value)
            else:                
                cm.value = decimal.Decimal(0)
                cm.comment = ''

        # 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(u'Must specify all or none of "attach_type", "attach_filename", "attach_data" in record for "%s"' % (recordid))
            am.file_attachment.save(name=file_filename, content=ContentFile(file_data), save=False)
            am.file_mediatype = file_mediatype
        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 = (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=activity, member=member)
            if grades:
                numeric_grade = grades[0]
                numeric_grade.flag = "GRAD"
            else:
                numeric_grade = NumericGrade(activity=activity, member=member, flag="GRAD")

            numeric_grade.value = value
            am.numeric_grade = numeric_grade
            numeric_grades.append(numeric_grade)

        else:
            group_members = GroupMember.objects.filter(group=group, activity=activity, confirmed=True)
            for g_member in group_members:
                try:            
                    ngrade = NumericGrade.objects.get(activity=activity, member=g_member.student)
                except NumericGrade.DoesNotExist: 
                    ngrade = NumericGrade(activity=activity, member=g_member.student)
                ngrade.value = value
                ngrade.flag = 'GRAD'
                numeric_grades.append(ngrade)

    return (activity_marks, activity_component_marks, numeric_grades)