Ejemplo n.º 1
0
class Student(User, CustomFieldModel):
    mname = models.CharField(max_length=150, blank=True, null=True, verbose_name="Middle Name")
    grad_date = models.DateField(blank=True, null=True, validators=settings.DATE_VALIDATORS)
    pic = ImageWithThumbsField(upload_to="student_pics", blank=True, null=True, sizes=((70,65),(530, 400)))
    alert = models.CharField(max_length=500, blank=True, help_text="Warn any user who accesses this record with this text")
    sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female')), blank=True, null=True)
    bday = models.DateField(blank=True, null=True, verbose_name="Birth Date", validators=settings.DATE_VALIDATORS)
    year = models.ForeignKey(
        GradeLevel,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        verbose_name="Grade level")
    class_of_year = models.ForeignKey(ClassYear, verbose_name="Graduating Class", blank=True, null=True)
    date_dismissed = models.DateField(blank=True, null=True, validators=settings.DATE_VALIDATORS)
    reason_left = models.ForeignKey(ReasonLeft, blank=True, null=True)
    unique_id = models.IntegerField(blank=True, null=True, unique=True, help_text="For integration with outside databases")
    ssn = models.CharField(max_length=11, blank=True, null=True)  #Once 1.1 is out USSocialSecurityNumberField(blank=True)

    # These fields are cached from emergency contacts
    parent_guardian = models.CharField(max_length=150, blank=True, editable=False)
    street = models.CharField(max_length=150, blank=True, editable=False)
    state = USStateField(blank=True, editable=False, null=True)
    city = models.CharField(max_length=255, blank=True)
    zip = models.CharField(max_length=10, blank=True, editable=False)
    parent_email = models.EmailField(blank=True, editable=False)

    family_preferred_language = models.ForeignKey(LanguageChoice, blank=True, null=True, default=get_default_language)
    family_access_users = models.ManyToManyField(
        family_ref,
        blank=True,
        related_name="+",
    )
    alt_email = models.EmailField(blank=True, help_text="Alternative student email that is not their school email.")
    notes = models.TextField(blank=True)
    emergency_contacts = models.ManyToManyField(EmergencyContact, verbose_name="Student Contact", blank=True)
    siblings = models.ManyToManyField('Student', blank=True)
    cohorts = models.ManyToManyField(Cohort, through='StudentCohort', blank=True)
    cache_cohort = models.ForeignKey(Cohort, editable=False, blank=True, null=True, on_delete=models.SET_NULL, help_text="Cached primary cohort.", related_name="cache_cohorts")
    individual_education_program = models.BooleanField(default=False)
    gpa = CachedDecimalField(editable=False, max_digits=5, decimal_places=2, blank=True, null=True)

    class Meta:
        permissions = (
            ("view_student", "View student"),
            ("view_ssn_student", "View student ssn"),
            ("view_mentor_student", "View mentoring information student"),
            ("reports", "View reports"),
        )
        ordering = ("last_name", "first_name")

    def __unicode__(self):
        return u"{0}, {1}".format(self.last_name, self.first_name)

    def get_absolute_url():
        pass

    # TC requested this for transcript template
    def get_long_grad_date(self):
        return self.grad_date.strftime('%B %d, %Y')

    def get_gpa(self, rounding=2, numeric_scale=False, boost=True):
        """ Get cached gpa but with rounding and scale options """
        gpa = self.gpa

        if numeric_scale == True:
            # Get the scale for the last year the student was active in
            grade_scale = GradeScale.objects.filter(
                schoolyear__markingperiod__coursesection__courseenrollment__user=self).last()
            if grade_scale:
                gpa = grade_scale.to_numeric(gpa)
                enrollments = self.courseenrollment_set.filter(
                        course_section__course__course_type__weight__gt=0)
                if boost:
                    boost_sum = enrollments.aggregate(boost_sum=Sum('course_section__course__course_type__boost'))['boost_sum']
                    boost_factor = boost_sum /  enrollments.count()
                    gpa += boost_factor
        if rounding:
            gpa = round_as_decimal(gpa, rounding)
        return gpa

    def calculate_gpa(self, date_report=None, rounding=2, prescale=False, boost=True):
        """ Use StudentYearGrade calculation
        No further weighting needed.
        """
        total = Decimal(0)
        years_with_grade = 0
        grade_years = self.studentyeargrade_set.filter(year__markingperiod__show_reports=True)
        if date_report:
            grade_years = grade_years.filter(year__start_date__lt=date_report)
        for grade_year in grade_years.distinct():

            # grade = grade_year.calculate_grade(date_report=date_report, prescale=prescale)

            grade = grade_year.get_grade(
                date_report = date_report,
                numeric_scale = True,
                rounding = rounding,
                prescale = prescale,
                boost = boost
                )
            if grade:
                # Is this an incomplete complete year?
                if date_report and date_report < grade_year.year.end_date:
                    # This year hasn't finished. What fraction is complete?
                    all_mps = grade_year.year.markingperiod_set.count()
                    complete_mps = grade_year.year.markingperiod_set.filter(
                        end_date__lte=date_report).count()
                    fraction = Decimal(complete_mps) / all_mps
                    total += grade * grade_year.credits * fraction
                    years_with_grade += grade_year.credits * fraction
                else:
                    total += grade * grade_year.credits
                    years_with_grade += grade_year.credits

        if years_with_grade:
            gpa = total / years_with_grade
            return round_as_decimal(gpa, decimal_places=rounding)

    @property
    def primary_cohort(self):
        return self.cache_cohort

    @property
    def phone(self):
        try:
            parent = self.emergency_contacts.order_by('-primary_contact')[0]
            return parent.emergencycontactnumber_set.all()[0].number
        except IndexError:
            return None

    @property
    def he_she(self):
        """ returns "he" or "she" """
        return self.gender_to_word("he", "she")

    @property
    def homeroom(self):
        """ Returns homeroom for student """
        from schedule.models import CourseSection
        try:
            courses = self.coursesection_set.filter(course__homeroom=True)
            homeroom = self.coursesection_set.get(course__homeroom=True)
        except:
            return ""

    @property
    def son_daughter(self):
        """ returns "son" or "daughter" """
        return self.gender_to_word("son", "daughter")

    @property
    def get_email(self):
        """ Returns email address using various configurable methods """
        email_method = Configuration.get_or_default(
            "How to obtain student email",
            default="append",
            help_text="append, user, or student.").value
        if email_method == "append":
            email_end = Configuration.get_or_default("email", default="@change.me").value
            return '%s%s' % (self.student.username, email_end)
        elif email_method == "user":
            if User.objects.filter(username=self.student.username):
                return User.objects.filter(username=self.student.username)[0].email
            return None
        return self.alt_email

    def get_phone_number(self):
        if self.studentnumber_set.filter(type="C"):
            return self.studentnumber_set.filter(type="C")[0]
        elif self.studentnumber_set.all():
            return self.studentnumber_set.all()[0]

    def get_primary_emergency_contact(self):
        if self.emergency_contacts.filter(primary_contact=True):
            return self.emergency_contacts.filter(primary_contact=True)[0]

    def get_disciplines(self, mps, action_name=None, count=True):
        """ Shortcut to look up discipline records
        mp: Marking Period
        action_name: Discipline action name
        count: Boolean - Just the count of them """
        if hasattr(mps,'db'): # More than one?
            if len(mps):
                start_date = mps.order_by('start_date')[0].start_date
                end_date = mps.order_by('-end_date')[0].end_date
                disc = self.studentdiscipline_set.filter(date__range=(start_date,end_date))
            else:
                disc = self.studentdiscipline_set.none()
        else:
            disc = self.studentdiscipline_set.filter(date__range=(mps.start_date,mps.end_date))
        if action_name:
            disc = disc.filter(action__name=action_name)
        if count:
            return disc.count()
        else:
            return disc

    def gender_to_word(self, male_word, female_word):
        """ returns a string based on the sex of student """
        if self.sex == "M":
            return male_word
        elif self.sex == "F":
            return female_word
        else:
            return male_word + "/" + female_word

    def cache_cohorts(self):
        cohorts = StudentCohort.objects.filter(student=self)
        if cohorts.filter(primary=True).count():
            self.cache_cohort = cohorts.filter(primary=True)[0].cohort
        elif cohorts.count():
            self.cache_cohort = cohorts[0].cohort
        else:
            self.cache_cohort = None

    def get_year(self, active_year):
        """ get the year (fresh, etc) from the class of XX year.
        """
        if self.class_of_year:
            try:
                this_year = active_year.end_date.year
                school_last_year = GradeLevel.objects.order_by('-id')[0].id
                class_of_year = self.class_of_year.year

                target_year = school_last_year - (class_of_year - this_year)
                return GradeLevel.objects.get(id=target_year)
            except:
                return None

    def get_scaled_multiple_mp_average_by_indices(self, indices, rounding=2):
        """ Get a scaled mulitple marking period average for this student
        Requires that the property mps be set previously.
        This function exists mainly for appy based report cards where speed,
        and simplicity (in the template) are important.
        """
        from ecwsp.grades.models import Grade
        mps = [ self.mps[i] for i in indices ]
        return Grade.get_scaled_multiple_mp_average(self, mps, rounding)


    def determine_year(self):
        """ Set the year (fresh, etc) from the class of XX year.
        """
        if self.class_of_year:
            try:
                active_year = SchoolYear.objects.filter(active_year=True)[0]
                self.year = self.get_year(active_year)
            except:
                return None

    def save(self, creating_worker=False, *args, **kwargs):
        self.cache_cohorts()
        if self.is_active == False and (Configuration.get_or_default("Clear Placement for Inactive Students","False").value == "True" \
        or Configuration.get_or_default("Clear Placement for Inactive Students","False").value == "true" \
        or Configuration.get_or_default("Clear Placement for Inactive Students","False").value == "T"):
            try:
                self.studentworker.placement = None
                self.studentworker.save()
            except: pass
        # Check year
        self.determine_year()
        super(Student, self).save(*args, **kwargs)

        # Create student worker if the app is installed.
        # https://code.djangoproject.com/ticket/7623
        if 'ecwsp.work_study' in settings.INSTALLED_APPS:
            if not creating_worker and not hasattr(self, 'studentworker'):
                from ecwsp.work_study.models import StudentWorker
                worker = StudentWorker(user_ptr_id=self.user_ptr_id)
                worker.__dict__.update(self.__dict__)
                worker.save(creating_worker=True)

        group, gcreated = Group.objects.get_or_create(name="students")
        self.user_ptr.groups.add(group)


    def clean(self, *args, **kwargs):
        """ Check if a Faculty exists, can't have someone be a Student and Faculty """
        if Faculty.objects.filter(id=self.id).count():
            raise ValidationError('Cannot have someone be a student AND faculty!')
        super(Student, self).clean(*args, **kwargs)

    def graduate_and_create_alumni(self):
        self.inactive = True
        self.reason_left = ReasonLeft.objects.get_or_create(reason="Graduated")[0]
        if not self.grad_date:
            self.grad_date = date.today()
        if 'ecwsp.alumni' in settings.INSTALLED_APPS:
            from ecwsp.alumni.models import Alumni
            Alumni.objects.get_or_create(student=self)
        self.save()

    def promote_to_worker(self):
        """ Promote student object to a student worker keeping all fields, does nothing on duplicate. """
        try:
            cursor = connection.cursor()
            cursor.execute("insert into work_study_studentworker (student_ptr_id) values (" + str(self.id) + ");")
        except:
            return
Ejemplo n.º 2
0
class CourseEnrollment(models.Model):
    course_section = models.ForeignKey('CourseSection')
    user = models.ForeignKey('sis.Student')
    attendance_note = models.CharField(
        max_length=255,
        blank=True,
        help_text="This note will appear when taking attendance.")
    exclude_days = models.CharField(
        max_length=100,
        blank=True,
        help_text=
        "Student does not need to attend on this day. Note course sections already specify meeting days; this field is for students who have a special reason to be away."
    )
    grade = CachedCharField(max_length=8,
                            blank=True,
                            verbose_name="Final Course Section Grade",
                            editable=False)
    numeric_grade = CachedDecimalField(max_digits=5,
                                       decimal_places=2,
                                       blank=True,
                                       null=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        unique_together = (("course_section", "user"), )

    def save(self, populate_all_grades=True, *args, **kwargs):
        """ populate_all_grades (default True) is intended to
        recalculate any related grades to this enrollment.
        It can be disabled to stop a recursive save.
        """
        super(CourseEnrollment, self).save(*args, **kwargs)
        if populate_all_grades is True:
            self.course_section.populate_all_grades()

    def cache_grades(self):
        """ Set cache on both grade and numeric_grade """
        grade = self.calculate_grade_real()
        if isinstance(grade, Decimal):
            grade = grade.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)
            self.numeric_grade = grade
        else:
            self.numeric_grade = None
        if grade == None:
            grade = ''
        self.grade = grade
        self.grade_recalculation_needed = False
        self.numeric_grade_recalculation_needed = False
        self.save(populate_all_grades=False)  # Causes recursion otherwise.
        return grade

    def get_average_for_marking_periods(self,
                                        marking_periods,
                                        letter=False,
                                        numeric=False):
        """ Get the average for only some marking periods
        marking_periods - Queryset or optionally pass ids only as an optimization
        letter - Letter grade scale
        numeric - non linear numeric scale
        """
        if isinstance(marking_periods, QuerySet):
            marking_periods = tuple(
                marking_periods.values_list('id', flat=True))
        else:
            # Check marking_periods because we can't use sql parameters because sqlite and django suck
            if all(isinstance(item, int) for item in marking_periods) != True:
                raise ValueError(
                    'marking_periods must be list or tuple of ints')
            marking_periods = tuple(marking_periods)

        cursor = connection.cursor()
        sql_string = '''
SELECT Sum(grade * weight) {over} / Sum(weight) {over} AS ave_grade FROM grades_grade
LEFT JOIN schedule_markingperiod
    ON schedule_markingperiod.id = grades_grade.marking_period_id
WHERE grades_grade.course_section_id = %s
    AND grades_grade.student_id = %s
    AND schedule_markingperiod.id in {marking_periods}
    AND ( grade IS NOT NULL OR letter_grade IS NOT NULL )'''
        if settings.DATABASES['default']['ENGINE'] in [
                'django.db.backends.postgresql_psycopg2',
                'tenant_schemas.postgresql_backend'
        ]:
            sql_string = sql_string.format(over='over ()',
                                           marking_periods=marking_periods)
        else:
            sql_string = sql_string.format(over='',
                                           marking_periods=marking_periods)

        cursor.execute(sql_string, (self.course_section_id, self.user_id))
        result = cursor.fetchone()
        if result:
            grade = result[0]
        else:
            return None
        if grade is not None:
            if letter:
                grade = self.optimized_grade_to_scale(grade, letter=True)
            elif numeric:
                grade = self.optimized_grade_to_scale(grade, letter=False)
        return grade

    def optimized_grade_to_scale(self, grade, letter=True):
        """ letter - True for letter grade, false for numeric (ex: 4.0 scale) """

        # not sure how this was working before, but I'm just commenting it out
        # if something else relies on the old method I have just broke it!
        # -Q
        '''rule = GradeScaleRule.objects.filter(
            grade_scale__schoolyear__markingperiod__coursesection=self,
            min_grade__lte=grade,
            max_grade__gte=grade).first()'''
        grade = round_to_standard(grade)
        rule = GradeScaleRule.objects.filter(
            min_grade__lte=grade,
            max_grade__gte=grade,
        ).select_related('course_section').first()
        if letter:
            return rule.letter_grade
        return rule.numeric_scale

    def get_grade(self, date_report=None, rounding=2, letter=False):
        """ Get the grade, use cache when no date change present
        date_report:
        rounding: Round to this many decimal places
        letter: Convert to letter grade scale
        """
        if date_report is None or date_report >= datetime.date.today():
            # Cache will always have the latest grade, so it's fine for
            # today's date and any future date
            if self.numeric_grade:
                grade = self.numeric_grade
            else:
                grade = self.grade
        else:
            grade = self.calculate_grade_real(date_report=date_report)
        if rounding and isinstance(grade,
                                   (int, long, float, complex, Decimal)):
            grade = round_as_decimal(grade, rounding)
        if letter == True and isinstance(grade,
                                         (int, long, float, complex, Decimal)):
            return self.optimized_grade_to_scale(grade)
        return grade

    def calculate_grade(self):
        return self.cache_grades()

    def calculate_numeric_grade(self):
        grade = self.cache_grades()
        if isinstance(grade, Decimal):
            return grade
        return None

    def calculate_grade_real(self, date_report=None, ignore_letter=False):
        """ Calculate the final grade for a course section
        ignore_letter can be useful when computing averages
        when you don't care about letter grades
        """
        cursor = connection.cursor()

        # postgres requires a over () to run
        # http://stackoverflow.com/questions/19271646/how-to-make-a-sum-without-group-by
        sql_string = '''
SELECT case when Sum(override_final{postgres_type_cast}) {over} = 1 then -9001 else (Sum(grade * weight) {over} / Sum(weight) {over}) end AS ave_grade
FROM grades_grade
    LEFT JOIN schedule_markingperiod
    ON schedule_markingperiod.id = grades_grade.marking_period_id
WHERE (grades_grade.course_section_id = %s
    AND grades_grade.student_id = %s {extra_where} )
    AND ( grade IS NOT NULL
    OR letter_grade IS NOT NULL )'''

        if date_report:
            if settings.DATABASES['default']['ENGINE'] in [
                    'django.db.backends.postgresql_psycopg2',
                    'tenant_schemas.postgresql_backend'
            ]:
                cursor.execute(
                    sql_string.format(
                        postgres_type_cast='::int',
                        over='over ()',
                        extra_where=
                        'AND (schedule_markingperiod.end_date <= %s OR override_final = true)'
                    ), (self.course_section_id, self.user_id, date_report))
            else:
                cursor.execute(
                    sql_string.format(
                        postgres_type_cast='',
                        over='',
                        extra_where=
                        'AND (schedule_markingperiod.end_date <= %s OR grades_grade.override_final = true)'
                    ), (self.course_section_id, self.user_id, date_report))

        else:
            if settings.DATABASES['default']['ENGINE'] in [
                    'django.db.backends.postgresql_psycopg2',
                    'tenant_schemas.postgresql_backend'
            ]:
                cursor.execute(
                    sql_string.format(postgres_type_cast='::int',
                                      over='over ()',
                                      extra_where=''),
                    (self.course_section_id, self.user_id))
            else:
                cursor.execute(
                    sql_string.format(postgres_type_cast='',
                                      over='',
                                      extra_where=''),
                    (self.course_section_id, self.user_id))

        result = cursor.fetchone()
        if result:
            ave_grade = result[0]
        else:  # No grades at all. The average of no grades is None
            return None

        # -9001 = override. We can't mix text and int in picky postgress.
        if str(ave_grade) == "-9001":
            course_section_grade = Grade.objects.get(
                override_final=True,
                student=self.user,
                course_section=self.course_section)
            grade = course_section_grade.get_grade()
            if ignore_letter and not isinstance(grade, (int, Decimal, float)):
                return None
            return grade

        elif ave_grade:
            return Decimal(ave_grade)

        # about 0.5 s
        # Letter Grade
        if ignore_letter == False:
            final = 0.0
            grades = self.course_section.grade_set.filter(student=self.user)
            if date_report:
                grades = grades.filter(
                    marking_period__end_date__lte=date_report)
            if grades:
                total_weight = Decimal(0)
                for grade in grades:
                    get_grade = grade.get_grade()
                    if get_grade in ["I", "IN", "YT"]:
                        return get_grade
                    elif get_grade in ["P", "HP", "LP"]:
                        if grade.marking_period:
                            final += float(100 * grade.marking_period.weight)
                            total_weight += grade.marking_period.weight
                    elif get_grade in ['F', 'M']:
                        if grade.marking_period:
                            total_weight += grade.marking_period.weight
                    elif get_grade:
                        try:
                            final += get_grade
                        except TypeError:
                            return get_grade
                if total_weight:
                    final /= float(total_weight)
                    final = Decimal(final).quantize(Decimal("0.01"),
                                                    ROUND_HALF_UP)
                    if final > config.LETTER_GRADE_REQUIRED_FOR_PASS:
                        return "P"
                    else:
                        return "F"
        return None
Ejemplo n.º 3
0
class StudentMarkingPeriodGrade(models.Model):
    """ Stores marking period grades for students, only used for cache """
    student = models.ForeignKey('sis.Student')
    marking_period = models.ForeignKey('schedule.MarkingPeriod',
                                       blank=True,
                                       null=True)
    grade = CachedDecimalField(max_digits=5,
                               decimal_places=2,
                               blank=True,
                               null=True,
                               verbose_name="MP Average")

    class Meta:
        unique_together = ('student', 'marking_period')

    def get_average(self, rounding=2):
        """ Returns cached average """
        return round_as_decimal(self.grade, rounding)

    def get_scaled_average(self, rounding=2, boost=True):
        """ Convert to scaled grade first, then average
        Burke Software does not endorse this as a precise way to calculate averages """
        grade_total = 0.0
        total_credits = 0.0
        grades = self.student.grade_set.filter(
            marking_period=self.marking_period,
            grade__isnull=False,
            course_section__course__course_type__weight__gt=0,
            enrollment__is_active=True,
        )

        for grade in grades:
            grade_value = float(grade.optimized_grade_to_scale(letter=False))
            if grade_value > 0 and boost:
                # only add boost for non-failing grades
                grade_value += float(
                    grade.course_section.course.course_type.boost)
            num_credits = float(grade.course_section.course.credits)
            grade_total += grade_value * num_credits
            total_credits += num_credits
        if total_credits > 0:
            average = grade_total / total_credits
            return round_as_decimal(average, rounding)
        else:
            return None

    @staticmethod
    def build_all_cache():
        """ Create object for each student * possible marking periods """
        for student in Student.objects.all():
            marking_periods = student.courseenrollment_set.values(
                'course_section__marking_period').annotate(
                    Count('course_section__marking_period'))
            for marking_period in marking_periods:
                StudentMarkingPeriodGrade.objects.get_or_create(
                    student=student,
                    marking_period_id=marking_period[
                        'course_section__marking_period'])

    def calculate_grade(self):
        cursor = connection.cursor()
        sql_string = """
select sum(grade * credits) / sum(credits * 1.0)
from grades_grade
left join schedule_coursesection on schedule_coursesection.id=grades_grade.course_section_id
left join schedule_course on schedule_coursesection.course_id=schedule_course.id
where marking_period_id=%s and student_id=%s and grade is not null;"""
        cursor.execute(sql_string, (self.marking_period_id, self.student_id))
        result = cursor.fetchone()
        if result:
            return result[0]
Ejemplo n.º 4
0
class CourseEnrollment(models.Model):
    course = models.ForeignKey('Course')
    user = models.ForeignKey('auth.User')
    role = models.CharField(max_length=255, default="Student", blank=True)
    attendance_note = models.CharField(
        max_length=255,
        blank=True,
        help_text="This note will appear when taking attendance.")
    year = models.ForeignKey('sis.GradeLevel', blank=True, null=True)
    exclude_days = models.ManyToManyField('Day', blank=True, \
        help_text="Student does not need to attend on this day. Note courses already specify meeting days; this field is for students who have a special reason to be away.")
    grade = CachedCharField(max_length=8,
                            blank=True,
                            verbose_name="Final Course Grade",
                            editable=False)
    numeric_grade = CachedDecimalField(max_digits=5,
                                       decimal_places=2,
                                       blank=True,
                                       null=True)

    class Meta:
        unique_together = (("course", "user", "role"), )

    def cache_grades(self):
        """ Set cache on both grade and numeric_grade """
        grade = self.calculate_grade_real()
        if isinstance(grade, Decimal):
            grade = grade.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)
            self.numeric_grade = grade
        else:
            self.numeric_grade = None
        if grade == None:
            grade = ''
        self.grade = grade
        self.grade_recalculation_needed = False
        self.numeric_grade_recalculation_needed = False
        self.save()
        return grade

    def get_grade(self, date_report=None, rounding=2):
        """ Get the grade, use cache when no date change present
        """
        if date_report is None or date_report >= datetime.date.today():
            # Cache will always have the latest grade, so it's fine for
            # today's date and any future date
            grade = self.grade
        else:
            grade = self.calculate_grade_real(date_report=date_report)
        if rounding and isinstance(grade,
                                   (int, long, float, complex, Decimal)):
            return round_as_decimal(grade, rounding)
        return grade

    def calculate_grade(self):
        return self.cache_grades()

    def calculate_numeric_grade(self):
        grade = self.cache_grades()
        if isinstance(grade, Decimal):
            return grade
        return None

    def calculate_grade_real(self, date_report=None, ignore_letter=False):
        """ Calculate the final grade for a course
        ignore_letter can be useful when computing averages
        when you don't care about letter grades
        """
        cursor = connection.cursor()

        # postgres requires a over () to run
        # http://stackoverflow.com/questions/19271646/how-to-make-a-sum-without-group-by
        sql_string = '''
SELECT ( Sum(grade * weight) {over} / Sum(weight) {over} ) AS ave_grade,
       grades_grade.id,
       grades_grade.override_final
FROM   grades_grade
       LEFT JOIN schedule_markingperiod
              ON schedule_markingperiod.id = grades_grade.marking_period_id
WHERE  ( grades_grade.course_id = %s
         AND grades_grade.student_id = %s {extra_where} )
       AND ( grade IS NOT NULL
              OR letter_grade IS NOT NULL )
ORDER  BY grades_grade.override_final DESC limit 1'''

        if date_report:
            if settings.DATABASES['default'][
                    'ENGINE'] == 'django.db.backends.postgresql_psycopg2':
                cursor.execute(
                    sql_string.format(
                        over='over ()',
                        extra_where=
                        'AND (schedule_markingperiod.end_date <= %s OR override_final = 1)'
                    ), (self.course_id, self.user_id, date_report))
            else:
                cursor.execute(
                    sql_string.format(
                        over='',
                        extra_where=
                        'AND (schedule_markingperiod.end_date <= %s OR grades_grade.override_final = 1)'
                    ), (self.course_id, self.user_id, date_report))

        else:
            if settings.DATABASES['default'][
                    'ENGINE'] == 'django.db.backends.postgresql_psycopg2':
                cursor.execute(
                    sql_string.format(over='over ()', extra_where=''),
                    (self.course_id, self.user_id))
            else:
                cursor.execute(sql_string.format(over='', extra_where=''),
                               (self.course_id, self.user_id))

        result = cursor.fetchone()
        if result:
            (ave_grade, grade_id, override_final) = result
        else:  # No grades at all. The average of no grades is None
            return None

        if override_final:
            course_grades = ecwsp.grades.models.Grade.objects.get(id=grade_id)
            grade = course_grades.get_grade()
            if ignore_letter and not isinstance(grade, (int, Decimal, float)):
                return None
            return grade

        if ave_grade:
            # database math always comes out as a float :(
            return Decimal(ave_grade)

        # about 0.5 s
        # Letter Grade
        if ignore_letter == False:
            final = 0.0
            grades = self.course.grade_set.filter(student=self.user)
            if date_report:
                grades = grades.filter(
                    marking_period__end_date__lte=date_report)
            if grades:
                total_weight = Decimal(0)
                for grade in grades:
                    get_grade = grade.get_grade()
                    if get_grade in ["I", "IN", "YT"]:
                        return get_grade
                    elif get_grade in ["P", "HP", "LP"]:
                        if grade.marking_period:
                            final += float(100 * grade.marking_period.weight)
                            total_weight += grade.marking_period.weight
                    elif get_grade in ['F', 'M']:
                        if grade.marking_period:
                            total_weight += grade.marking_period.weight
                    elif get_grade:
                        final += get_grade
                if total_weight:
                    final /= float(total_weight)
                    final = Decimal(final).quantize(Decimal("0.01"),
                                                    ROUND_HALF_UP)
                    if final > int(
                            Configuration.get_or_default(
                                'letter_grade_required_for_pass').value):
                        return "P"
                    else:
                        return "F"
        return None
Ejemplo n.º 5
0
class StudentYearGrade(models.Model):
    """ Stores the grade for an entire year, only used for cache """
    student = models.ForeignKey('sis.Student')
    year = models.ForeignKey('sis.SchoolYear')
    grade = CachedDecimalField(max_digits=5,
                               decimal_places=2,
                               blank=True,
                               null=True,
                               verbose_name="Year average")
    credits = CachedDecimalField(max_digits=5,
                                 decimal_places=2,
                                 blank=True,
                                 null=True)

    class Meta:
        unique_together = ('student', 'year')

    @staticmethod
    def build_cache_student(student):
        years = student.courseenrollment_set.values(
            'course_section__marking_period__school_year').annotate(
                Count('course_section__marking_period__school_year'))
        for year in years:
            if year['course_section__marking_period__school_year']:
                year_grade = StudentYearGrade.objects.get_or_create(
                    student=student,
                    year_id=year['course_section__marking_period__school_year']
                )[0]
                if year_grade.credits_recalculation_needed:
                    year_grade.recalculate_credits()
                if year_grade.grade_recalculation_needed:
                    year_grade.recalculate_grade()

    @staticmethod
    def build_all_cache(*args, **kwargs):
        """ Create object for each student * possible years """
        if 'instance' in kwargs:
            StudentYearGrade.build_cache_student(kwargs['instance'])
        else:
            for student in Student.objects.all():
                StudentYearGrade.build_cache_student(student)

    def calculate_credits(self):
        """ The number of credits a student has earned in 1 year """
        return self.calculate_grade_and_credits()[1]

    def calculate_grade_and_credits(self, date_report=None, prescale=False):
        """ Just recalculate them both at once
        returns (grade, credits)

        if "prescale=True" grades are scaled (i.e. 4.0) before averaged
        """
        total = Decimal(0)
        credits = Decimal(0)
        prescaled_grade = Decimal(0)
        grade_scale = self.year.grade_scale
        for course_enrollment in self.student.courseenrollment_set.filter(
                course_section__marking_period__show_reports=True,
                course_section__marking_period__school_year=self.year,
                course_section__course__credits__isnull=False,
                course_section__course__course_type__weight__gt=0,
        ).distinct():
            grade = course_enrollment.calculate_grade_real(
                date_report=date_report, ignore_letter=True)
            if grade:
                num_credits = course_enrollment.course_section.course.credits
                if prescale:
                    # scale the grades before averaging them if requested
                    if grade_scale:
                        grade = grade_scale.to_numeric(grade)
                    prescaled_grade += grade * num_credits
                total += grade * num_credits
                credits += num_credits
        if credits > 0:
            grade = total / credits
            if prescale:
                prescaled_grade = prescaled_grade / credits
        else:
            grade = None
        if date_report == None:  # If set would indicate this is not for cache!
            self.grade = grade
            self.credits = credits
            self.grade_recalculation_needed = False
            self.credits_recalculation_needed = False
            self.save()

        if prescale:
            return (prescaled_grade, credits)
        else:
            return (grade, credits)

    def calculate_grade(self, date_report=None, prescale=False):
        """ Calculate grade considering MP weights and course credits
        course_enrollment.calculate_real_grade returns a MP weighted result,
        so just have to consider credits
        """
        return self.calculate_grade_and_credits(date_report=date_report,
                                                prescale=prescale)[0]

    def get_grade(self,
                  date_report=None,
                  rounding=2,
                  numeric_scale=False,
                  prescale=False,
                  boost=True):
        if numeric_scale == False and (date_report is None or
                                       date_report >= datetime.date.today()):
            # Cache will always have the latest grade, so it's fine for
            # today's date and any future date
            return self.grade
        grade = self.calculate_grade(date_report=date_report,
                                     prescale=prescale)
        if numeric_scale == True:
            grade_scale = self.year.grade_scale
            if grade_scale and not prescale:
                grade = grade_scale.to_numeric(grade)
            if boost:
                enrollments = self.student.courseenrollment_set.filter(
                    course_section__marking_period__show_reports=True,
                    course_section__marking_period__school_year=self.year,
                    course_section__course__credits__isnull=False,
                    course_section__course__course_type__weight__gt=0,
                )
                if not grade_scale:
                    boost_sum = enrollments.aggregate(boost_sum=Sum(
                        'course_section__course__course_type__boost')
                                                      )['boost_sum']
                    if not boost_sum:
                        boost_sum = 0.0
                    try:
                        boost_factor = boost_sum / enrollments.count()
                    except ZeroDivisionError:
                        boost_factor = 0.0
                else:
                    boost_sum = 0.0
                    total_credits = 0.0
                    for enrollment in enrollments:
                        course_credits = enrollment.course_section.course.credits
                        course_boost = enrollment.course_section.course.course_type.boost
                        if enrollment.numeric_grade:
                            course_grade = Decimal(enrollment.numeric_grade)
                            total_credits += float(course_credits)
                            if grade_scale.to_numeric(course_grade) > 0:
                                # only add boost to grades that are not failing...
                                boost_sum += float(course_boost *
                                                   course_credits)

                    try:
                        boost_factor = boost_sum / total_credits
                    except ZeroDivisionError:
                        boost_factor = None
                if enrollments.count() > 0 and boost_factor and grade:
                    grade = float(grade) + float(boost_factor)
        if rounding:
            grade = round_as_decimal(grade, rounding)
        return grade