Example #1
0
def gradebook_get_category_average(student, category, marking_period):
    try:
        agg = benchmark_get_or_flush(student.aggregate_set,
                                     course_section=None,
                                     category=category,
                                     marking_period=marking_period)
    except Aggregate.DoesNotExist:
        agg, created = benchmark_calculate_category_as_course_aggregate(
            student, category, marking_period)
    if agg.cached_substitution is not None:
        return agg.cached_substitution
    elif agg.cached_value is not None:
        calculation_rule = benchmark_find_calculation_rule(
            marking_period.school_year)
        if category.display_scale is not None:
            pretty = agg.cached_value / agg._fallback_points_possible(
            ) * category.display_scale
            pretty = '{}{}'.format(
                pretty.quantize(
                    Decimal(10)**(-1 * calculation_rule.decimal_places),
                    ROUND_HALF_UP), category.display_symbol)
        else:
            pretty = agg.cached_value.quantize(
                Decimal(10)**(-1 * calculation_rule.decimal_places),
                ROUND_HALF_UP)
        return pretty
    else:
        return None
Example #2
0
def benchmark_calculate_category_as_course_aggregate(student, category, marking_period):
    agg, created = benchmark_get_create_or_flush(student.aggregate_set, course=None, category=category, marking_period=marking_period)
    agg.name = 'G! {} - {} (All Courses, {})'.format(student, category, marking_period)
    agg.cached_substitution = None
    calculation_rule = benchmark_find_calculation_rule(marking_period.school_year)
    category_as_course = calculation_rule.category_as_course_set.get(category=category)
    category_numer = category_denom = Decimal(0)
    for course in Course.objects.filter(award_credits=True, courseenrollment__user__username=student.username, marking_period=marking_period, department__in=category_as_course.include_departments.all()).distinct():
        credits = Decimal(course.credits) / course.marking_period.count()
        try:
            category_aggregate = benchmark_get_or_flush(Aggregate, student=student, marking_period=marking_period, category=category, course=course)
        except Aggregate.DoesNotExist:
            category_aggregate = benchmark_calculate_course_category_aggregate(student, course, category, marking_period)[0]
        if category_aggregate is not None and category_aggregate.cached_value is not None:
            calculate_as, display_as = calculation_rule.substitute(category_aggregate, category_aggregate.cached_value)
            category_numer += credits * calculate_as
            category_denom += credits
            # yes, agg will just end up with the last substitution, but tough
            if display_as is not None and len(display_as):
                agg.cached_substitution = display_as
    if category_denom:
        agg.cached_value = category_numer / category_denom
    else:
        agg.cached_value = None
    agg.save()
    return agg, created
Example #3
0
def gradebook_get_average_and_pk(
    student, course, category=None, marking_period=None, items=None, omit_substitutions=False
):
    try:
        if items is not None:  # averages of one-off sets of items aren't saved and must be calculated every time
            # this is rather silly, but it avoids code duplication or a teensy four-line function.
            raise Aggregate.DoesNotExist
        agg = benchmark_get_or_flush(
            student.aggregate_set, course=course, category=category, marking_period=marking_period
        )
    except Aggregate.DoesNotExist:
        if category is None:
            agg, created = benchmark_calculate_course_aggregate(student, course, marking_period, items)
        else:
            agg, created = benchmark_calculate_course_category_aggregate(
                student, course, category, marking_period, items
            )
    if not omit_substitutions and agg.cached_substitution is not None:
        return agg.cached_substitution, agg.pk
    elif agg.cached_value is not None:
        calculation_rule = benchmark_find_calculation_rule(course.marking_period.all()[0].school_year)
        if category is not None and category.display_scale is not None:
            pretty = agg.cached_value / agg._fallback_points_possible() * category.display_scale
            pretty = "{}{}".format(
                pretty.quantize(Decimal(10) ** (-1 * calculation_rule.decimal_places), ROUND_HALF_UP),
                category.display_symbol,
            )
        else:
            pretty = agg.cached_value.quantize(Decimal(10) ** (-1 * calculation_rule.decimal_places), ROUND_HALF_UP)
        return pretty, agg.pk
    else:
        return None, agg.pk
Example #4
0
def benchmark_calculate_course_aggregate(student, course, marking_period, items=None, recalculate_all_categories=False):
    # doesn't recalculate component aggregates by default
    if items is None:
        # QUICK HACK to use new Aggregate calculation method
        # TODO: Subclass Aggregate and override mark_set for one-off sets of Items
        agg, created = benchmark_get_create_or_flush(
            Aggregate, student=student, course=course, marking_period=marking_period, category=None
        )
        agg.calculate(recalculate_all_categories)
        return agg, created
        # /HACK (haha, right.)

        # just leave items alone--we don't actually consider it here; we only pass it to benchmark_calculate_course_category_aggregate
        # setting items here will prevent benchmark_calculate_course_category_aggregate from saving anything
        save = True
        items_categories = ()
    else:
        # don't store aggregates for every one-off combination of items
        save = False
        # we'll have to miss cache and recaculate any category to which an item belongs
        items_categories = Category.objects.filter(item__in=items).distinct()

    calculation_rule = benchmark_find_calculation_rule(course.marking_period.all()[0].school_year)

    # initialize attributes
    criteria = {"course": course, "category": None, "marking_period": marking_period}
    # silly name is silly, and should not be part of the criteria
    silly_name = "G! {} - Course Average ({}, {})".format(student, course, marking_period)
    # don't use get_or_create; otherwise we may end up saving an empty object
    try:
        agg = benchmark_get_or_flush(student.aggregate_set, **criteria)
        created = False
    except Aggregate.DoesNotExist:
        agg = Aggregate(student=student, **criteria)
        created = True
    agg.name = silly_name

    # begin the actual calculations!
    agg.cached_substitution = None
    course_numer = course_denom = Decimal(0)
    for rule_category in calculation_rule.per_course_category_set.filter(apply_to_departments=course.department):
        criteria["category"] = rule_category.category
        cat_agg, cat_created = benchmark_get_create_or_flush(student.aggregate_set, **criteria)
        if cat_created or recalculate_all_categories or rule_category.category in items_categories:
            cat_agg, cat_created = benchmark_calculate_course_category_aggregate(
                student, course, rule_category.category, marking_period, items
            )
        if cat_agg.cached_value is not None:
            course_numer += rule_category.weight * cat_agg.cached_value
            course_denom += rule_category.weight
            # yes, agg will just end up with the last substitution, but tough
            if cat_agg.cached_substitution is not None:
                agg.cached_substitution = cat_agg.cached_substitution
    if course_denom:
        agg.cached_value = course_numer / course_denom
    else:
        agg.cached_value = None
    if save:
        agg.save()
    return agg, created
Example #5
0
def benchmark_calculate_course_category_aggregate(student, course, category, marking_period, items=None):
    if items is None:
        items = Item.objects.all()
        save = True
    else:
        # don't store aggregates for every one-off combination of items
        save = False
    items = items.filter(course=course, category=category)
    # if we're passed marking_period=None, we should consider items across the entire duration of the course
    # if we're passed a specific marking period instead, we should consider items matching only that marking period
    if marking_period is not None:
        items = items.filter(marking_period=marking_period)

    calculation_rule = benchmark_find_calculation_rule(course.marking_period.all()[0].school_year)

    # initialize attributes
    criteria = {"course": course, "category": category, "marking_period": marking_period}
    # silly name is silly, and should not be part of the criteria
    silly_name = "G! {} - {} ({}, {})".format(student, category, course, marking_period)
    # don't use get_or_create; otherwise we may end up saving an empty object
    try:
        agg = benchmark_get_or_flush(student.aggregate_set, **criteria)
        created = False
    except Aggregate.DoesNotExist:
        agg = Aggregate(student=student, **criteria)
        created = True
    agg.name = silly_name

    # begin the actual calculations!
    agg.cached_substitution = None
    category_numer = category_denom = Decimal(0)
    if category.allow_multiple_demonstrations:
        for category_item in items.exclude(points_possible=None):
            # Find the highest mark amongst demonstrations and count it as the grade for the item
            best = Mark.objects.filter(student=student, item=category_item).aggregate(Max("mark"))["mark__max"]
            if best is not None:
                calculate_as, display_as = calculation_rule.substitute(category_item, best)
                category_numer += calculate_as
                category_denom += category_item.points_possible
                # yes, agg will just end up with the last substitution, but tough
                if display_as is not None:
                    agg.cached_substitution = display_as

    else:
        for category_mark in (
            Mark.objects.filter(student=student, item__in=items).exclude(mark=None).exclude(item__points_possible=None)
        ):
            calculate_as, display_as = calculation_rule.substitute(category_mark.item, category_mark.mark)
            category_numer += calculate_as
            category_denom += category_mark.item.points_possible
            if display_as is not None:
                agg.cached_substitution = display_as
    if category_denom:
        agg.cached_value = category_numer / category_denom * agg._fallback_points_possible()
    else:
        agg.cached_value = None
    if save:
        agg.save()
    return agg, created
Example #6
0
def benchmark_calculate_category_as_course_aggregate(student, category,
                                                     marking_period):
    agg, created = benchmark_get_create_or_flush(student.aggregate_set,
                                                 course_section=None,
                                                 category=category,
                                                 marking_period=marking_period)
    agg.name = 'G! {} - {} (All Courses, {})'.format(student, category,
                                                     marking_period)
    agg.cached_substitution = None
    calculation_rule = benchmark_find_calculation_rule(
        marking_period.school_year)
    category_as_course = calculation_rule.category_as_course_set.get(
        category=category)
    category_numer = category_denom = Decimal(0)
    for course_section in CourseSection.objects.filter(
            course__course_type__award_credits=True,
            courseenrollment__user__username=student.username,
            marking_period=marking_period,
            department__in=category_as_course.include_departments.all(
            )).distinct():
        credits = Decimal(
            course_section.credits) / course_section.marking_period.count()
        try:
            category_aggregate = benchmark_get_or_flush(
                Aggregate,
                student=student,
                marking_period=marking_period,
                category=category,
                course_section=course_section)
        except Aggregate.DoesNotExist:
            category_aggregate = benchmark_calculate_course_category_aggregate(
                student, course_section, category, marking_period)[0]
        if category_aggregate is not None and category_aggregate.cached_value is not None:
            calculate_as, display_as = calculation_rule.substitute(
                category_aggregate, category_aggregate.cached_value)
            category_numer += credits * calculate_as
            category_denom += credits
            # yes, agg will just end up with the last substitution, but tough
            if display_as is not None and len(display_as):
                agg.cached_substitution = display_as
    if category_denom:
        agg.cached_value = category_numer / category_denom
    else:
        agg.cached_value = None
    agg.save()
    return agg, created
Example #7
0
def gradebook_get_category_average(student, category, marking_period):
    try:
        agg = benchmark_get_or_flush(student.aggregate_set, course=None, category=category, marking_period=marking_period)
    except Aggregate.DoesNotExist:
        agg, created = benchmark_calculate_category_as_course_aggregate(student, category, marking_period)
    if agg.cached_substitution is not None:
        return agg.cached_substitution
    elif agg.cached_value is not None:
        calculation_rule = benchmark_find_calculation_rule(marking_period.school_year)
        if category.display_scale is not None:
            pretty = agg.cached_value / agg._fallback_points_possible() * category.display_scale
            pretty = '{}{}'.format(pretty.quantize(Decimal(10) ** (-1 * calculation_rule.decimal_places), ROUND_HALF_UP), category.display_symbol)
        else:
            pretty = agg.cached_value.quantize(Decimal(10) ** (-1 * calculation_rule.decimal_places), ROUND_HALF_UP)
        return pretty
    else:
        return None
Example #8
0
def gradebook_get_average_and_pk(student,
                                 course_section,
                                 category=None,
                                 marking_period=None,
                                 items=None,
                                 omit_substitutions=False):
    try:
        if items is not None:  # averages of one-off sets of items aren't saved and must be calculated every time
            # this is rather silly, but it avoids code duplication or a teensy four-line function.
            raise Aggregate.DoesNotExist
        agg = benchmark_get_or_flush(student.aggregate_set,
                                     course_section=course_section,
                                     category=category,
                                     marking_period=marking_period)
    except Aggregate.DoesNotExist:
        if category is None:
            agg, created = benchmark_calculate_course_aggregate(
                student, course_section, marking_period, items)
        else:
            agg, created = benchmark_calculate_course_category_aggregate(
                student, course_section, category, marking_period, items)
    if not omit_substitutions and agg.cached_substitution is not None:
        return agg.cached_substitution, agg.pk
    elif agg.cached_value is not None:
        calculation_rule = benchmark_find_calculation_rule(
            course_section.marking_period.all()[0].school_year)
        if category is not None and category.display_scale is not None:
            pretty = agg.cached_value / agg._fallback_points_possible(
            ) * category.display_scale
            pretty = '{}{}'.format(
                pretty.quantize(
                    Decimal(10)**(-1 * calculation_rule.decimal_places),
                    ROUND_HALF_UP), category.display_symbol)
        else:
            pretty = agg.cached_value.quantize(
                Decimal(10)**(-1 * calculation_rule.decimal_places),
                ROUND_HALF_UP)
        return pretty, agg.pk
    else:
        return None, agg.pk
Example #9
0
def benchmark_calculate_course_aggregate(student,
                                         course_section,
                                         marking_period,
                                         items=None,
                                         recalculate_all_categories=False):
    # doesn't recalculate component aggregates by default
    if items is None:
        # QUICK HACK to use new Aggregate calculation method
        # TODO: Subclass Aggregate and override mark_set for one-off sets of Items
        agg, created = benchmark_get_create_or_flush(
            Aggregate,
            student=student,
            course_section=course_section,
            marking_period=marking_period,
            category=None)
        agg.calculate(recalculate_all_categories)
        return agg, created
        # /HACK (haha, right.)

        # just leave items alone--we don't actually consider it here; we only pass it to benchmark_calculate_course_category_aggregate
        # setting items here will prevent benchmark_calculate_course_category_aggregate from saving anything
        save = True
        items_categories = ()
    else:
        # don't store aggregates for every one-off combination of items
        save = False
        # we'll have to miss cache and recaculate any category to which an item belongs
        items_categories = Category.objects.filter(item__in=items).distinct()

    calculation_rule = benchmark_find_calculation_rule(
        course_section.marking_period.all()[0].school_year)

    # initialize attributes
    criteria = {
        'course_section': course_section,
        'category': None,
        'marking_period': marking_period
    }
    # silly name is silly, and should not be part of the criteria
    silly_name = 'G! {} - Course Average ({}, {})'.format(
        student, course_section, marking_period)
    # don't use get_or_create; otherwise we may end up saving an empty object
    try:
        agg = benchmark_get_or_flush(student.aggregate_set, **criteria)
        created = False
    except Aggregate.DoesNotExist:
        agg = Aggregate(student=student, **criteria)
        created = True
    agg.name = silly_name

    # begin the actual calculations!
    agg.cached_substitution = None
    course_section_numer = course_section_denom = Decimal(0)
    for rule_category in calculation_rule.per_course_category_set.filter(
            apply_to_departments=course_section.department):
        criteria['category'] = rule_category.category
        cat_agg, cat_created = benchmark_get_create_or_flush(
            student.aggregate_set, **criteria)
        if cat_created or recalculate_all_categories or rule_category.category in items_categories:
            cat_agg, cat_created = benchmark_calculate_course_category_aggregate(
                student, course_section, rule_category.category,
                marking_period, items)
        if cat_agg.cached_value is not None:
            course_section_numer += rule_category.weight * cat_agg.cached_value
            course_section_denom += rule_category.weight
            # yes, agg will just end up with the last substitution, but tough
            if cat_agg.cached_substitution is not None:
                agg.cached_substitution = cat_agg.cached_substitution
    if course_section_denom:
        agg.cached_value = course_section_numer / course_section_denom
    else:
        agg.cached_value = None
    if save:
        agg.save()
    return agg, created
Example #10
0
def benchmark_calculate_course_category_aggregate(student,
                                                  course_section,
                                                  category,
                                                  marking_period,
                                                  items=None):
    if items is None:
        items = Item.objects.all()
        save = True
    else:
        # don't store aggregates for every one-off combination of items
        save = False
    items = items.filter(course_section=course_section, category=category)
    # if we're passed marking_period=None, we should consider items across the entire duration of the course section
    # if we're passed a specific marking period instead, we should consider items matching only that marking period
    if marking_period is not None:
        items = items.filter(marking_period=marking_period)

    calculation_rule = benchmark_find_calculation_rule(
        course_section.marking_period.all()[0].school_year)

    # initialize attributes
    criteria = {
        'course_section': course_section,
        'category': category,
        'marking_period': marking_period
    }
    # silly name is silly, and should not be part of the criteria
    silly_name = 'G! {} - {} ({}, {})'.format(student, category,
                                              course_section, marking_period)
    # don't use get_or_create; otherwise we may end up saving an empty object
    try:
        agg = benchmark_get_or_flush(student.aggregate_set, **criteria)
        created = False
    except Aggregate.DoesNotExist:
        agg = Aggregate(student=student, **criteria)
        created = True
    agg.name = silly_name

    # begin the actual calculations!
    agg.cached_substitution = None
    category_numer = category_denom = Decimal(0)
    if category.allow_multiple_demonstrations:
        for category_item in items.exclude(points_possible=None):
            # Find the highest mark amongst demonstrations and count it as the grade for the item
            best = Mark.objects.filter(student=student,
                                       item=category_item).aggregate(
                                           Max('mark'))['mark__max']
            if best is not None:
                calculate_as, display_as = calculation_rule.substitute(
                    category_item, best)
                category_numer += calculate_as
                category_denom += category_item.points_possible
                # yes, agg will just end up with the last substitution, but tough
                if display_as is not None:
                    agg.cached_substitution = display_as

    else:
        for category_mark in Mark.objects.filter(
                student=student, item__in=items).exclude(mark=None).exclude(
                    item__points_possible=None):
            calculate_as, display_as = calculation_rule.substitute(
                category_mark.item, category_mark.mark)
            category_numer += calculate_as
            category_denom += category_mark.item.points_possible
            if display_as is not None:
                agg.cached_substitution = display_as
    if category_denom:
        agg.cached_value = category_numer / category_denom * agg._fallback_points_possible(
        )
    else:
        agg.cached_value = None
    if save:
        agg.save()
    return agg, created
Example #11
0
def benchmark_calculate_grade_for_courses(student, courses, marking_period=None, date_report=None):
    # TODO: Decimal places configuration value
    DECIMAL_PLACES = 2
    # student: a single student
    # courses: all courses involved in the GPA calculation
    # marking_period: restricts GPA calculation to a _single_ marking period
    # date_report: restricts GPA calculation to marking periods _ending_ on or before a date

    mps = None
    if marking_period is not None:
        mps = MarkingPeriod.objects.filter(id=(marking_period.id))
    else:
        mps = MarkingPeriod.objects.filter(id__in=courses.values('marking_period').distinct())
        if date_report is not None:
            mps = mps.filter(end_date__lte=date_report)
        else:
            mps = course.marking_period.all()

    student_numer = student_denom = float(0)
    for mp in mps.filter(school_year__benchmark_grade=True):
        mp_numer = mp_denom = float(0)
        rule = benchmark_find_calculation_rule(mp.school_year)
        for course in courses.filter(marking_period=mp).exclude(credits=None).distinct(): # IMO, Course.credits should be required, and we should not treat None as 0.
            # Handle per-course categories according to the calculation rule
            course_numer = course_denom = float(0)
            for category in rule.per_course_category_set.filter(apply_to_departments=course.department):
                try: category_aggregate = benchmark_get_or_flush(Aggregate, student=student, marking_period=mp, course=course, category=category.category)
                except Aggregate.DoesNotExist: category_aggregate = None
                if category_aggregate is not None and category_aggregate.cached_value is not None:
                    # simplified normalization; assumes minimum is 0
                    normalized_value = category_aggregate.cached_value / rule.points_possible
                    course_numer += float(category.weight) * float(normalized_value)
                    course_denom += float(category.weight)
            if course_denom > 0:
                credits = float(course.credits) / course.marking_period.count()
                mp_numer += credits * course_numer / course_denom
                mp_denom += credits

        # Handle aggregates of categories that are counted as courses
        # TODO: Change CalculationRule model to have a field for the weight of each category. For now, assume 1.
        # Categories as courses shouldn't increase the weight of a marking period!
        mp_denom_before_categories = mp_denom
        for category in rule.category_as_course_set.all():
            category_numer = category_denom = float(0)
            for course in courses.filter(marking_period=mp, department__in=category.include_departments.all()).distinct():
                credits = float(course.credits) / course.marking_period.count()
                try: category_aggregate = benchmark_get_or_flush(Aggregate, student=student, marking_period=mp, category=category.category, course=course)
                except Aggregate.DoesNotExist: category_aggregate = None
                if category_aggregate is not None and category_aggregate.cached_value is not None:
                    # simplified normalization; assumes minimum is 0
                    normalized_value  = category_aggregate.cached_value / rule.points_possible
                    category_numer += credits * float(normalized_value)
                    category_denom += credits
            if category_denom > 0:
                mp_numer += category_numer / category_denom
                mp_denom += 1

        if mp_denom > 0:
            mp_numer *= float(rule.points_possible)
            student_numer += mp_numer / mp_denom * mp_denom_before_categories 
            student_denom += mp_denom_before_categories
            mp_denom = mp_denom_before_categories # in this version, mp_denom isn't used again, but this may save someone pain in the future.

    # Handle non-benchmark-grade years. Calculation rules don't apply.
    legacy_courses = courses.filter(marking_period__in=mps.filter(school_year__benchmark_grade=False))
    for course in legacy_courses.exclude(credits=None).distinct(): # IMO, Course.credits should be required, and we should not treat None as 0.
        try:
            grade, credits = student._calculate_grade_for_single_course(course, marking_period, date_report)
            student_numer += grade * credits
            student_denom += credits
        except:
            logging.warning('Legacy course grade calculation failed for student {}, course {}, marking_period {}, date_report {}'.format(student, course, marking_period, date_report), exc_info=True)
            
    if student_denom > 0:
        return Decimal(student_numer / student_denom).quantize(Decimal(10) ** (-1 * DECIMAL_PLACES), ROUND_HALF_UP)
    else:
        return 'N/A'