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
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
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
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
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
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
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
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
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
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
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'