"""
Models for the grades app
"""
import uuid
from django.contrib.auth.models import User
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.utils import IntegrityError

from courses.models import (
    Course,
    CourseRun,
    Program,
)
from exams.models import ExamRun
from exams.constants import EXAM_GRADE_PASS, EXAM_GRADE_FAIL
from grades.constants import FinalGradeStatus
from micromasters.models import (
    AuditableModel,
    AuditModel,
    TimestampedModel,
)
from micromasters.utils import (
    now_in_utc,
    serialize_model_object,
)

class CourseRunGradingAlreadyCompleteError(Exception):
    """
    Custom Exception to raise in case a change of status for a course run is attempted
    """

class FinalGradeQuerySet(models.QuerySet):
    """
    QuerySet class defining common query parameters for FinalGrade
    """
    def passed(self):
        """
        Returns a queryset with a filter that will only fetch passed course runs
        """
        return self.filter(passed=True)

    def paid_on_edx(self):
        """
        Returns a queryset with a filter that will only fetch course runs that are paid on edX.
        Note that a course run can still be paid via an Order if this value is False for a given user.
        """
        return self.filter(course_run_paid_on_edx=True)

    def for_course_run_key(self, edx_course_key):
        """
        Returns a queryset with a filter that will only fetch course runs that match a course key
        """
        return self.filter(course_run__edx_course_key=edx_course_key)

    def for_course_run_keys(self, edx_course_keys):
        """
        Returns a queryset with a filter that will only fetch course runs that match one of many course keys
        """
        return self.filter(course_run__edx_course_key__in=edx_course_keys)

class FinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store edx final grades
    """
    # Set a custom manager for this model. This lets us do a couple useful things:
    # (1) Apply commonly-needed filters by a short, easily-recognized name, and (2) compose a query piece-by-piece.
    objects = FinalGradeQuerySet.as_manager()

    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course_run = models.ForeignKey(CourseRun, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        null=True,
        validators=[MinValueValidator(0.0)]
    )
    passed = models.BooleanField(default=False)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )
    course_run_paid_on_edx = models.BooleanField(default=False)

    class Meta:
        unique_together = ('user', 'course_run')

    @classmethod
    def get_audit_class(cls):
        return FinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    @property
    def grade_percent(self):
        """Returns the grade field value as a number out of 100 (or None if the value is None)"""
        return self.grade * 100 if self.grade is not None else None

    @classmethod
    def get_frozen_users(cls, course_run):
        """
        Returns all the users with a final grade in a given course run
        """
        return list(
            cls.objects.filter(
                course_run=course_run, status=FinalGradeStatus.COMPLETE
            ).values_list('user', flat=True)
        )

    def __str__(self):
        return 'Grade in course "{course_id}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_id=self.course_run.edx_course_key
        )

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden method to run validation"""
        self.full_clean()
        return super().save(*args, **kwargs)

class FinalGradeAudit(AuditModel):
    """
    Audit table for the Final Grade
    """
    final_grade = models.ForeignKey(FinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'final_grade'

    def __str__(self):
        return 'Grade audit for user "{user}", course "{course_id}"'.format(
            user=self.final_grade.user,
            course_id=self.final_grade.course_run.edx_course_key
        )

class MicromastersCourseCertificate(TimestampedModel):
    """
    Model for storing MicroMasters course certificates
    """
    user = models.ForeignKey(User, models.SET_NULL, null=True, related_name='course_certificates')
    course = models.ForeignKey(Course, models.SET_NULL, null=True)
    hash = models.CharField(max_length=32, null=False, unique=True)

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Course Certificate for user={user}, course={course}, hash="{hash}"'.format(
            user=self.user,
            course=self.course.id,
            hash=self.hash,
        )

class MicromastersProgramCertificate(TimestampedModel):
    """
    Model for storing MicroMasters program certificates
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    hash = models.CharField(max_length=32, null=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Program Certificate for user={user}, program={program}, hash="{hash}"'.format(
            user=self.user,
            program=self.program,
            hash=self.hash,
        )

class MicromastersProgramCommendation(TimestampedModel):
    """
    Model for storing MicroMasters program congratulation letters
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def __str__(self):
        return 'Program letter for user={user}, program={program}, uuid="{uuid}"'.format(
            user=self.user,
            program=self.program,
            uuid=self.uuid,
        )

class CourseRunGradingStatus(TimestampedModel):
    """
    Additional information for the course run related to the final grades
    """
    course_run = models.OneToOneField(CourseRun, null=False, on_delete=models.CASCADE)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )

    def __str__(self):
        return 'Freezing status "{status}" for course "{course_id}"'.format(
            course_id=self.course_run.edx_course_key,
            status=self.status
        )

    @classmethod
    def is_complete(cls, course_run):
        """
        Returns True if there is an entry with status 'complete'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.COMPLETE).exists()

    @classmethod
    def is_pending(cls, course_run):
        """
        Returns True if there is an entry with status 'pending'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.PENDING).exists()

    @classmethod
    def set_to_complete(cls, course_run):
        """
        Sets the status for the course_run to complete
        """
        course_run_grade_status, _ = cls.objects.get_or_create(course_run=course_run)
        if course_run_grade_status.status != FinalGradeStatus.COMPLETE:
            course_run_grade_status.status = FinalGradeStatus.COMPLETE
            course_run_grade_status.save()

    @classmethod
    def create_pending(cls, course_run):
        """
        Creates an entry with status pending
        """
        try:
            course_fg_info, _ = cls.objects.get_or_create(course_run=course_run, status=FinalGradeStatus.PENDING)
        except IntegrityError:
            raise CourseRunGradingAlreadyCompleteError(
                'Course Run "{0}" has already been completed'.format(course_run.edx_course_key))
        return course_fg_info

class ProctoredExamGrade(TimestampedModel, AuditableModel):
    """
    Model to store proctored exam grades (like the pearson exams)
    """
    # relationship to other models
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    exam_run = models.ForeignKey(ExamRun, null=True, on_delete=models.CASCADE)

    # fields from proctorate exam results
    exam_date = models.DateTimeField()
    passing_score = models.FloatField()
    score = models.FloatField()
    grade = models.TextField()
    client_authorization_id = models.TextField(null=True, blank=True)

    # dump of all the remaining fields coming from the proctorate exam results
    row_data = JSONField(null=False)

    # custom fields based on proctorate exam results
    passed = models.BooleanField()
    percentage_grade = models.FloatField(null=False)

    @classmethod
    def get_audit_class(cls):
        return ProctoredExamGradeAudit

    @classmethod
    def for_user_course(cls, user, course):
        """
        Returns a queryset of the exam result for an user in a course of a program
        """
        now = now_in_utc()
        return cls.objects.filter(user=user, course=course, exam_run__date_grades_available__lte=now)

    def set_score(self, new_score):
        """
        Sets a value for the score field, then sets related fields based on that value

        Args:
            new_score (float): The new score value
        """
        self.score = new_score
        self.percentage_grade = self.score / 100.0
        self.passed = self.score >= self.passing_score
        self.grade = EXAM_GRADE_PASS if self.passed else EXAM_GRADE_FAIL

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Proctored Exam Grade in course "{course_title}", user "{user}"'.format(
            user=self.user.username,
            course_title=self.course.title
        )

class ProctoredExamGradeAudit(AuditModel):
    """
    Audit table for the ProctoredExamGrade
    """
    proctored_exam_grade = models.ForeignKey(ProctoredExamGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'proctored_exam_grade'

    def __str__(self):
        return 'Proctored Exam Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.proctored_exam_grade.user,
            course_title=self.proctored_exam_grade.course.title
        )

class CombinedFinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store a combined grade for course_run and an exam
    """
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]
    )

    class Meta:
        unique_together = ('user', 'course')

    @classmethod
    def get_audit_class(cls):
        return CombinedFinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Combined Final Grade in course "{course_title}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_title=self.course.title
        )

class CombinedFinalGradeAudit(AuditModel):
    """
    Audit table for CombinedFinalGrade
    """
    combined_final_grade = models.ForeignKey(CombinedFinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'combined_final_grade'

    def __str__(self):
        return 'Combined Final Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.combined_final_grade.user,
            course_title=self.combined_final_grade.course.title
        )


"""
Models for the grades app
"""
import uuid
from django.contrib.auth.models import User
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.utils import IntegrityError

from courses.models import (
    Course,
    CourseRun,
    Program,
)
from exams.models import ExamRun
from exams.constants import EXAM_GRADE_PASS, EXAM_GRADE_FAIL
from grades.constants import FinalGradeStatus
from micromasters.models import (
    AuditableModel,
    AuditModel,
    TimestampedModel,
)
from micromasters.utils import (
    now_in_utc,
    serialize_model_object,
)

class CourseRunGradingAlreadyCompleteError(Exception):
    """
    Custom Exception to raise in case a change of status for a course run is attempted
    """

class FinalGradeQuerySet(models.QuerySet):
    """
    QuerySet class defining common query parameters for FinalGrade
    """
    def passed(self):
        """
        Returns a queryset with a filter that will only fetch passed course runs
        """
        return self.filter(passed=True)

    def paid_on_edx(self):
        """
        Returns a queryset with a filter that will only fetch course runs that are paid on edX.
        Note that a course run can still be paid via an Order if this value is False for a given user.
        """
        return self.filter(course_run_paid_on_edx=True)

    def for_course_run_key(self, edx_course_key):
        """
        Returns a queryset with a filter that will only fetch course runs that match a course key
        """
        return self.filter(course_run__edx_course_key=edx_course_key)

    def for_course_run_keys(self, edx_course_keys):
        """
        Returns a queryset with a filter that will only fetch course runs that match one of many course keys
        """
        return self.filter(course_run__edx_course_key__in=edx_course_keys)

class FinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store edx final grades
    """
    # Set a custom manager for this model. This lets us do a couple useful things:
    # (1) Apply commonly-needed filters by a short, easily-recognized name, and (2) compose a query piece-by-piece.
    objects = FinalGradeQuerySet.as_manager()

    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course_run = models.ForeignKey(CourseRun, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        null=True,
        validators=[MinValueValidator(0.0)]
    )
    passed = models.BooleanField(default=False)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )
    course_run_paid_on_edx = models.BooleanField(default=False)

    class Meta:
        unique_together = ('user', 'course_run')

    @classmethod
    def get_audit_class(cls):
        return FinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    @property
    def grade_percent(self):
        """Returns the grade field value as a number out of 100 (or None if the value is None)"""
        return self.grade * 100 if self.grade is not None else None

    @classmethod
    def get_frozen_users(cls, course_run):
        """
        Returns all the users with a final grade in a given course run
        """
        return list(
            cls.objects.filter(
                course_run=course_run, status=FinalGradeStatus.COMPLETE
            ).values_list('user', flat=True)
        )

    def __str__(self):
        return 'Grade in course "{course_id}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_id=self.course_run.edx_course_key
        )

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden method to run validation"""
        self.full_clean()
        return super().save(*args, **kwargs)

class FinalGradeAudit(AuditModel):
    """
    Audit table for the Final Grade
    """
    final_grade = models.ForeignKey(FinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'final_grade'

    def __str__(self):
        return 'Grade audit for user "{user}", course "{course_id}"'.format(
            user=self.final_grade.user,
            course_id=self.final_grade.course_run.edx_course_key
        )

class MicromastersCourseCertificate(TimestampedModel):
    """
    Model for storing MicroMasters course certificates
    """
    user = models.ForeignKey(User, models.SET_NULL, null=True, related_name='course_certificates')
    course = models.ForeignKey(Course, models.SET_NULL, null=True)
    hash = models.CharField(max_length=32, null=False, unique=True)

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Course Certificate for user={user}, course={course}, hash="{hash}"'.format(
            user=self.user,
            course=self.course.id,
            hash=self.hash,
        )

class MicromastersProgramCertificate(TimestampedModel):
    """
    Model for storing MicroMasters program certificates
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    hash = models.CharField(max_length=32, null=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Program Certificate for user={user}, program={program}, hash="{hash}"'.format(
            user=self.user,
            program=self.program,
            hash=self.hash,
        )

class MicromastersProgramCommendation(TimestampedModel):
    """
    Model for storing MicroMasters program congratulation letters
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def __str__(self):
        return 'Program letter for user={user}, program={program}, uuid="{uuid}"'.format(
            user=self.user,
            program=self.program,
            uuid=self.uuid,
        )

class CourseRunGradingStatus(TimestampedModel):
    """
    Additional information for the course run related to the final grades
    """
    course_run = models.OneToOneField(CourseRun, null=False, on_delete=models.CASCADE)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )

    def __str__(self):
        return 'Freezing status "{status}" for course "{course_id}"'.format(
            course_id=self.course_run.edx_course_key,
            status=self.status
        )

    @classmethod
    def is_complete(cls, course_run):
        """
        Returns True if there is an entry with status 'complete'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.COMPLETE).exists()

    @classmethod
    def is_pending(cls, course_run):
        """
        Returns True if there is an entry with status 'pending'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.PENDING).exists()

    @classmethod
    def set_to_complete(cls, course_run):
        """
        Sets the status for the course_run to complete
        """
        course_run_grade_status, _ = cls.objects.get_or_create(course_run=course_run)
        if course_run_grade_status.status != FinalGradeStatus.COMPLETE:
            course_run_grade_status.status = FinalGradeStatus.COMPLETE
            course_run_grade_status.save()
        return course_run

    @classmethod
    def create_pending(cls, course_run):
        """
        Creates an entry with status pending
        """
        try:
            course_fg_info, _ = cls.objects.get_or_create(course_run=course_run, status=FinalGradeStatus.PENDING)
        except IntegrityError:
            raise CourseRunGradingAlreadyCompleteError(
                'Course Run "{0}" has already been completed'.format(course_run.edx_course_key))
        return course_fg_info

class ProctoredExamGrade(TimestampedModel, AuditableModel):
    """
    Model to store proctored exam grades (like the pearson exams)
    """
    # relationship to other models
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    exam_run = models.ForeignKey(ExamRun, null=True, on_delete=models.CASCADE)

    # fields from proctorate exam results
    exam_date = models.DateTimeField()
    passing_score = models.FloatField()
    score = models.FloatField()
    grade = models.TextField()
    client_authorization_id = models.TextField(null=True, blank=True)

    # dump of all the remaining fields coming from the proctorate exam results
    row_data = JSONField(null=False)

    # custom fields based on proctorate exam results
    passed = models.BooleanField()
    percentage_grade = models.FloatField(null=False)

    @classmethod
    def get_audit_class(cls):
        return ProctoredExamGradeAudit

    @classmethod
    def for_user_course(cls, user, course):
        """
        Returns a queryset of the exam result for an user in a course of a program
        """
        now = now_in_utc()
        return cls.objects.filter(user=user, course=course, exam_run__da

    def set_score(self, new_score):
        """
        Sets a value for the score field, then sets related fields based on that value

        Args:
            new_score (float): The new score value
        """
        self.score = new_score
        self.percentage_grade = self.score / 100.0
        self.passed = self.score >= self.passing_score
        self.grade = EXAM_GRADE_PASS if self.passed else EXAM_GRADE_FAIL

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Proctored Exam Grade in course "{course_title}", user "{user}"'.format(
            user=self.user.username,
            course_title=self.course.title
        )

class ProctoredExamGradeAudit(AuditModel):
    """
    Audit table for the ProctoredExamGrade
    """
    proctored_exam_grade = models.ForeignKey(ProctoredExamGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'proctored_exam_grade'

    def __str__(self):
        return 'Proctored Exam Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.proctored_exam_grade.user,
            course_title=self.proctored_exam_grade.course.title
        )

class CombinedFinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store a combined grade for course_run and an exam
    """
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]
    )

    class Meta:
        unique_together = ('user', 'course')

    @classmethod
    def get_audit_class(cls):
        return CombinedFinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Combined Final Grade in course "{course_title}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_title=self.course.title
        )

class CombinedFinalGradeAudit(AuditModel):
    """
    Audit table for CombinedFinalGrade
    """
    combined_final_grade = models.ForeignKey(CombinedFinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'combined_final_grade'

    def __str__(self):
        return 'Combined Final Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.combined_final_grade.user,
            course_title=self.combined_final_grade.course.title
        )


        self.score = new_score
        self.percentage_grade = self.score / 100.0
        self.passed = self.score >= self.passing_score
        self.grade = EXAM_GRADE_PASS if self.passed else EXAM_GRADE_FAIL

"""
Models for the grades app
"""
import uuid
from django.contrib.auth.models import User
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.utils import IntegrityError

from courses.models import (
    Course,
    CourseRun,
    Program,
)
from exams.models import ExamRun
from exams.constants import EXAM_GRADE_PASS, EXAM_GRADE_FAIL
from grades.constants import FinalGradeStatus
from micromasters.models import (
    AuditableModel,
    AuditModel,
    TimestampedModel,
)
from micromasters.utils import (
    now_in_utc,
    serialize_model_object,
)

class CourseRunGradingAlreadyCompleteError(Exception):
    """
    Custom Exception to raise in case a change of status for a course run is attempted
    """

class FinalGradeQuerySet(models.QuerySet):
    """
    QuerySet class defining common query parameters for FinalGrade
    """
    def passed(self):
        """
        Returns a queryset with a filter that will only fetch passed course runs
        """
        return self.filter(passed=True)

    def paid_on_edx(self):
        """
        Returns a queryset with a filter that will only fetch course runs that are paid on edX.
        Note that a course run can still be paid via an Order if this value is False for a given user.
        """
        return self.filter(course_run_paid_on_edx=True)

    def for_course_run_key(self, edx_course_key):
        """
        Returns a queryset with a filter that will only fetch course runs that match a course key
        """
        return self.filter(course_run__edx_course_key=edx_course_key)

    def for_course_run_keys(self, edx_course_keys):
        """
        Returns a queryset with a filter that will only fetch course runs that match one of many course keys
        """
        return self.filter(course_run__edx_course_key__in=edx_course_keys)

class FinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store edx final grades
    """
    # Set a custom manager for this model. This lets us do a couple useful things:
    # (1) Apply commonly-needed filters by a short, easily-recognized name, and (2) compose a query piece-by-piece.
    objects = FinalGradeQuerySet.as_manager()

    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course_run = models.ForeignKey(CourseRun, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        null=True,
        validators=[MinValueValidator(0.0)]
    )
    passed = models.BooleanField(default=False)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )
    course_run_paid_on_edx = models.BooleanField(default=False)

    class Meta:
        unique_together = ('user', 'course_run')

    @classmethod
    def get_audit_class(cls):
        return FinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    @property
    def grade_percent(self):
        """Returns the grade field value as a number out of 100 (or None if the value is None)"""
        return self.grade * 100 if self.grade is not None else None

    @classmethod
    def get_frozen_users(cls, course_run):
        """
        Returns all the users with a final grade in a given course run
        """
        return list(
            cls.objects.filter(
                course_run=course_run, status=FinalGradeStatus.COMPLETE
            ).values_list('user', flat=True)
        )

    def __str__(self):
        return 'Grade in course "{course_id}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_id=self.course_run.edx_course_key
        )

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden method to run validation"""
        self.full_clean()
        return super().save(*args, **kwargs)

class FinalGradeAudit(AuditModel):
    """
    Audit table for the Final Grade
    """
    final_grade = models.ForeignKey(FinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'final_grade'

    def __str__(self):
        return 'Grade audit for user "{user}", course "{course_id}"'.format(
            user=self.final_grade.user,
            course_id=self.final_grade.course_run.edx_course_key
        )

class MicromastersCourseCertificate(TimestampedModel):
    """
    Model for storing MicroMasters course certificates
    """
    user = models.ForeignKey(User, models.SET_NULL, null=True, related_name='course_certificates')
    course = models.ForeignKey(Course, models.SET_NULL, null=True)
    hash = models.CharField(max_length=32, null=False, unique=True)

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Course Certificate for user={user}, course={course}, hash="{hash}"'.format(
            user=self.user,
            course=self.course.id,
            hash=self.hash,
        )

class MicromastersProgramCertificate(TimestampedModel):
    """
    Model for storing MicroMasters program certificates
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    hash = models.CharField(max_length=32, null=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Program Certificate for user={user}, program={program}, hash="{hash}"'.format(
            user=self.user,
            program=self.program,
            hash=self.hash,
        )

class MicromastersProgramCommendation(TimestampedModel):
    """
    Model for storing MicroMasters program congratulation letters
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def __str__(self):
        return 'Program letter for user={user}, program={program}, uuid="{uuid}"'.format(
            user=self.user,
            program=self.program,
            uuid=self.uuid,
        )

class CourseRunGradingStatus(TimestampedModel):
    """
    Additional information for the course run related to the final grades
    """
    course_run = models.OneToOneField(CourseRun, null=False, on_delete=models.CASCADE)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )

    def __str__(self):
        return 'Freezing status "{status}" for course "{course_id}"'.format(
            course_id=self.course_run.edx_course_key,
            status=self.status
        )

    @classmethod
    def is_complete(cls, course_run):
        """
        Returns True if there is an entry with status 'complete'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.COMPLETE).exists()

    @classmethod
    def is_pending(cls, course_run):
        """
        Returns True if there is an entry with status 'pending'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.PENDING).exists()

    @classmethod
    def set_to_complete(cls, course_run):
        """
        Sets the status for the course_run to complete
        """
        course_run_grade_status, _ = cls.objects.get_or_create(course_run=course_run)
        if course_run_grade_status.status != FinalGradeStatus.COMPLETE:
            course_run_grade_status.status = FinalGradeStatus.COMPLETE
            course_run_grade_status.save()
        return course_run

    @classmethod
    def create_pending(cls, course_run):
        """
        Creates an entry with status pending
        """
        try:
            course_fg_info, _ = cls.objects.get_or_create(course_run=course_run, status=FinalGradeStatus.PENDING)
        except IntegrityError:
            raise CourseRunGradingAlreadyCompleteError(
                'Course Run "{0}" has already been completed'.format(course_run.edx_course_key))
        return course_fg_info

class ProctoredExamGrade(TimestampedModel, AuditableModel):
    """
    Model to store proctored exam grades (like the pearson exams)
    """
    # relationship to other models
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    exam_run = models.ForeignKey(ExamRun, null=True, on_delete=models.CASCADE)

    # fields from proctorate exam results
    exam_date = models.DateTimeField()
    passing_score = models.FloatField()
    score = models.FloatField()
    grade = models.TextField()
    client_authorization_id = models.TextField(null=True, blank=True)

    # dump of all the remaining fields coming from the proctorate exam results
    row_data = JSONField(null=False)

    # custom fields based on proctorate exam results
    passed = models.BooleanField()
    percentage_grade = models.FloatField(null=False)

    @classmethod
    def get_audit_class(cls):
        return ProctoredExamGradeAudit

    @classmethod
    def for_user_course(cls, user, course):
        """
        Returns a queryset of the exam result for an user in a course of a program
        """
        now = now_in_utc()
        return cls.objects.filter(user=user, course=course, exam_run__date_grades_available__lte=now)

    def set_score(self, new_score):
        """
        Sets a value for the score field, then sets related fields based on that value

        Args:
            new_score (float): The new score value
        """

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Proctored Exam Grade in course "{course_title}", user "{user}"'.format(
            user=self.user.username,
            course_title=self.course.title
        )

class ProctoredExamGradeAudit(AuditModel):
    """
    Audit table for the ProctoredExamGrade
    """
    proctored_exam_grade = models.ForeignKey(ProctoredExamGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'proctored_exam_grade'

    def __str__(self):
        return 'Proctored Exam Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.proctored_exam_grade.user,
            course_title=self.proctored_exam_grade.course.title
        )

class CombinedFinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store a combined grade for course_run and an exam
    """
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]
    )

    class Meta:
        unique_together = ('user', 'course')

    @classmethod
    def get_audit_class(cls):
        return CombinedFinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Combined Final Grade in course "{course_title}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_title=self.course.title
        )

class CombinedFinalGradeAudit(AuditModel):
    """
    Audit table for CombinedFinalGrade
    """
    combined_final_grade = models.ForeignKey(CombinedFinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'combined_final_grade'

    def __str__(self):
        return 'Combined Final Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.combined_final_grade.user,
            course_title=self.combined_final_grade.course.title
        )


if course_run_grade_status.status != FinalGradeStatus.COMPLETE:
            course_run_grade_status.status = FinalGradeStatus.COMPLETE
            course_run_grade_status.save()

"""
Models for the grades app
"""
import uuid
from django.contrib.auth.models import User
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.utils import IntegrityError

from courses.models import (
    Course,
    CourseRun,
    Program,
)
from exams.models import ExamRun
from exams.constants import EXAM_GRADE_PASS, EXAM_GRADE_FAIL
from grades.constants import FinalGradeStatus
from micromasters.models import (
    AuditableModel,
    AuditModel,
    TimestampedModel,
)
from micromasters.utils import (
    now_in_utc,
    serialize_model_object,
)

class CourseRunGradingAlreadyCompleteError(Exception):
    """
    Custom Exception to raise in case a change of status for a course run is attempted
    """

class FinalGradeQuerySet(models.QuerySet):
    """
    QuerySet class defining common query parameters for FinalGrade
    """
    def passed(self):
        """
        Returns a queryset with a filter that will only fetch passed course runs
        """
        return self.filter(passed=True)

    def paid_on_edx(self):
        """
        Returns a queryset with a filter that will only fetch course runs that are paid on edX.
        Note that a course run can still be paid via an Order if this value is False for a given user.
        """
        return self.filter(course_run_paid_on_edx=True)

    def for_course_run_key(self, edx_course_key):
        """
        Returns a queryset with a filter that will only fetch course runs that match a course key
        """
        return self.filter(course_run__edx_course_key=edx_course_key)

    def for_course_run_keys(self, edx_course_keys):
        """
        Returns a queryset with a filter that will only fetch course runs that match one of many course keys
        """
        return self.filter(course_run__edx_course_key__in=edx_course_keys)

class FinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store edx final grades
    """
    # Set a custom manager for this model. This lets us do a couple useful things:
    # (1) Apply commonly-needed filters by a short, easily-recognized name, and (2) compose a query piece-by-piece.
    objects = FinalGradeQuerySet.as_manager()

    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course_run = models.ForeignKey(CourseRun, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        null=True,
        validators=[MinValueValidator(0.0)]
    )
    passed = models.BooleanField(default=False)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )
    course_run_paid_on_edx = models.BooleanField(default=False)

    class Meta:
        unique_together = ('user', 'course_run')

    @classmethod
    def get_audit_class(cls):
        return FinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    @property
    def grade_percent(self):
        """Returns the grade field value as a number out of 100 (or None if the value is None)"""
        return self.grade * 100 if self.grade is not None else None

    @classmethod
    def get_frozen_users(cls, course_run):
        """
        Returns all the users with a final grade in a given course run
        """
        return list(
            cls.objects.filter(
                course_run=course_run, status=FinalGradeStatus.COMPLETE
            ).values_list('user', flat=True)
        )

    def __str__(self):
        return 'Grade in course "{course_id}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_id=self.course_run.edx_course_key
        )

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden method to run validation"""
        self.full_clean()
        return super().save(*args, **kwargs)

class FinalGradeAudit(AuditModel):
    """
    Audit table for the Final Grade
    """
    final_grade = models.ForeignKey(FinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'final_grade'

    def __str__(self):
        return 'Grade audit for user "{user}", course "{course_id}"'.format(
            user=self.final_grade.user,
            course_id=self.final_grade.course_run.edx_course_key
        )

class MicromastersCourseCertificate(TimestampedModel):
    """
    Model for storing MicroMasters course certificates
    """
    user = models.ForeignKey(User, models.SET_NULL, null=True, related_name='course_certificates')
    course = models.ForeignKey(Course, models.SET_NULL, null=True)
    hash = models.CharField(max_length=32, null=False, unique=True)

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Course Certificate for user={user}, course={course}, hash="{hash}"'.format(
            user=self.user,
            course=self.course.id,
            hash=self.hash,
        )

class MicromastersProgramCertificate(TimestampedModel):
    """
    Model for storing MicroMasters program certificates
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    hash = models.CharField(max_length=32, null=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Program Certificate for user={user}, program={program}, hash="{hash}"'.format(
            user=self.user,
            program=self.program,
            hash=self.hash,
        )

class MicromastersProgramCommendation(TimestampedModel):
    """
    Model for storing MicroMasters program congratulation letters
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def __str__(self):
        return 'Program letter for user={user}, program={program}, uuid="{uuid}"'.format(
            user=self.user,
            program=self.program,
            uuid=self.uuid,
        )

class CourseRunGradingStatus(TimestampedModel):
    """
    Additional information for the course run related to the final grades
    """
    course_run = models.OneToOneField(CourseRun, null=False, on_delete=models.CASCADE)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )

    def __str__(self):
        return 'Freezing status "{status}" for course "{course_id}"'.format(
            course_id=self.course_run.edx_course_key,
            status=self.status
        )

    @classmethod
    def is_complete(cls, course_run):
        """
        Returns True if there is an entry with status 'complete'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.COMPLETE).exists()

    @classmethod
    def is_pending(cls, course_run):
        """
        Returns True if there is an entry with status 'pending'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.PENDING).exists()

    @classmethod
    def set_to_complete(cls, course_run):
        """
        Sets the status for the course_run to complete
        """
        course_run_grade_status, _ = cls.objects.get_or_create(course_run=course_run)

        return course_run

    @classmethod
    def create_pending(cls, course_run):
        """
        Creates an entry with status pending
        """
        try:
            course_fg_info, _ = cls.objects.get_or_create(course_run=course_run, status=FinalGradeStatus.PENDING)
        except IntegrityError:
            raise CourseRunGradingAlreadyCompleteError(
                'Course Run "{0}" has already been completed'.format(course_run.edx_course_key))
        return course_fg_info

class ProctoredExamGrade(TimestampedModel, AuditableModel):
    """
    Model to store proctored exam grades (like the pearson exams)
    """
    # relationship to other models
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    exam_run = models.ForeignKey(ExamRun, null=True, on_delete=models.CASCADE)

    # fields from proctorate exam results
    exam_date = models.DateTimeField()
    passing_score = models.FloatField()
    score = models.FloatField()
    grade = models.TextField()
    client_authorization_id = models.TextField(null=True, blank=True)

    # dump of all the remaining fields coming from the proctorate exam results
    row_data = JSONField(null=False)

    # custom fields based on proctorate exam results
    passed = models.BooleanField()
    percentage_grade = models.FloatField(null=False)

    @classmethod
    def get_audit_class(cls):
        return ProctoredExamGradeAudit

    @classmethod
    def for_user_course(cls, user, course):
        """
        Returns a queryset of the exam result for an user in a course of a program
        """
        now = now_in_utc()
        return cls.objects.filter(user=user, course=course, exam_run__date_grades_available__lte=now)

    def set_score(self, new_score):
        """
        Sets a value for the score field, then sets related fields based on that value

        Args:
            new_score (float): The new score value
        """
        self.score = new_score
        self.percentage_grade = self.score / 100.0
        self.passed = self.score >= self.passing_score
        self.grade = EXAM_GRADE_PASS if self.passed else EXAM_GRADE_FAIL

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Proctored Exam Grade in course "{course_title}", user "{user}"'.format(
            user=self.user.username,
            course_title=self.course.title
        )

class ProctoredExamGradeAudit(AuditModel):
    """
    Audit table for the ProctoredExamGrade
    """
    proctored_exam_grade = models.ForeignKey(ProctoredExamGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'proctored_exam_grade'

    def __str__(self):
        return 'Proctored Exam Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.proctored_exam_grade.user,
            course_title=self.proctored_exam_grade.course.title
        )

class CombinedFinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store a combined grade for course_run and an exam
    """
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]
    )

    class Meta:
        unique_together = ('user', 'course')

    @classmethod
    def get_audit_class(cls):
        return CombinedFinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Combined Final Grade in course "{course_title}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_title=self.course.title
        )

class CombinedFinalGradeAudit(AuditModel):
    """
    Audit table for CombinedFinalGrade
    """
    combined_final_grade = models.ForeignKey(CombinedFinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'combined_final_grade'

    def __str__(self):
        return 'Combined Final Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.combined_final_grade.user,
            course_title=self.combined_final_grade.course.title
        )


try:
            course_fg_info, _ = cls.objects.get_or_create(course_run=course_run, status=FinalGradeStatus.PENDING)
        except IntegrityError:
            raise CourseRunGradingAlreadyCompleteError(
                'Course Run "{0}" has already been completed'.format(course_run.edx_course_key))

"""
Models for the grades app
"""
import uuid
from django.contrib.auth.models import User
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.utils import IntegrityError

from courses.models import (
    Course,
    CourseRun,
    Program,
)
from exams.models import ExamRun
from exams.constants import EXAM_GRADE_PASS, EXAM_GRADE_FAIL
from grades.constants import FinalGradeStatus
from micromasters.models import (
    AuditableModel,
    AuditModel,
    TimestampedModel,
)
from micromasters.utils import (
    now_in_utc,
    serialize_model_object,
)

class CourseRunGradingAlreadyCompleteError(Exception):
    """
    Custom Exception to raise in case a change of status for a course run is attempted
    """

class FinalGradeQuerySet(models.QuerySet):
    """
    QuerySet class defining common query parameters for FinalGrade
    """
    def passed(self):
        """
        Returns a queryset with a filter that will only fetch passed course runs
        """
        return self.filter(passed=True)

    def paid_on_edx(self):
        """
        Returns a queryset with a filter that will only fetch course runs that are paid on edX.
        Note that a course run can still be paid via an Order if this value is False for a given user.
        """
        return self.filter(course_run_paid_on_edx=True)

    def for_course_run_key(self, edx_course_key):
        """
        Returns a queryset with a filter that will only fetch course runs that match a course key
        """
        return self.filter(course_run__edx_course_key=edx_course_key)

    def for_course_run_keys(self, edx_course_keys):
        """
        Returns a queryset with a filter that will only fetch course runs that match one of many course keys
        """
        return self.filter(course_run__edx_course_key__in=edx_course_keys)

class FinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store edx final grades
    """
    # Set a custom manager for this model. This lets us do a couple useful things:
    # (1) Apply commonly-needed filters by a short, easily-recognized name, and (2) compose a query piece-by-piece.
    objects = FinalGradeQuerySet.as_manager()

    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course_run = models.ForeignKey(CourseRun, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        null=True,
        validators=[MinValueValidator(0.0)]
    )
    passed = models.BooleanField(default=False)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )
    course_run_paid_on_edx = models.BooleanField(default=False)

    class Meta:
        unique_together = ('user', 'course_run')

    @classmethod
    def get_audit_class(cls):
        return FinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    @property
    def grade_percent(self):
        """Returns the grade field value as a number out of 100 (or None if the value is None)"""
        return self.grade * 100 if self.grade is not None else None

    @classmethod
    def get_frozen_users(cls, course_run):
        """
        Returns all the users with a final grade in a given course run
        """
        return list(
            cls.objects.filter(
                course_run=course_run, status=FinalGradeStatus.COMPLETE
            ).values_list('user', flat=True)
        )

    def __str__(self):
        return 'Grade in course "{course_id}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_id=self.course_run.edx_course_key
        )

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden method to run validation"""
        self.full_clean()
        return super().save(*args, **kwargs)

class FinalGradeAudit(AuditModel):
    """
    Audit table for the Final Grade
    """
    final_grade = models.ForeignKey(FinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'final_grade'

    def __str__(self):
        return 'Grade audit for user "{user}", course "{course_id}"'.format(
            user=self.final_grade.user,
            course_id=self.final_grade.course_run.edx_course_key
        )

class MicromastersCourseCertificate(TimestampedModel):
    """
    Model for storing MicroMasters course certificates
    """
    user = models.ForeignKey(User, models.SET_NULL, null=True, related_name='course_certificates')
    course = models.ForeignKey(Course, models.SET_NULL, null=True)
    hash = models.CharField(max_length=32, null=False, unique=True)

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Course Certificate for user={user}, course={course}, hash="{hash}"'.format(
            user=self.user,
            course=self.course.id,
            hash=self.hash,
        )

class MicromastersProgramCertificate(TimestampedModel):
    """
    Model for storing MicroMasters program certificates
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    hash = models.CharField(max_length=32, null=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Program Certificate for user={user}, program={program}, hash="{hash}"'.format(
            user=self.user,
            program=self.program,
            hash=self.hash,
        )

class MicromastersProgramCommendation(TimestampedModel):
    """
    Model for storing MicroMasters program congratulation letters
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def __str__(self):
        return 'Program letter for user={user}, program={program}, uuid="{uuid}"'.format(
            user=self.user,
            program=self.program,
            uuid=self.uuid,
        )

class CourseRunGradingStatus(TimestampedModel):
    """
    Additional information for the course run related to the final grades
    """
    course_run = models.OneToOneField(CourseRun, null=False, on_delete=models.CASCADE)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )

    def __str__(self):
        return 'Freezing status "{status}" for course "{course_id}"'.format(
            course_id=self.course_run.edx_course_key,
            status=self.status
        )

    @classmethod
    def is_complete(cls, course_run):
        """
        Returns True if there is an entry with status 'complete'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.COMPLETE).exists()

    @classmethod
    def is_pending(cls, course_run):
        """
        Returns True if there is an entry with status 'pending'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.PENDING).exists()

    @classmethod
    def set_to_complete(cls, course_run):
        """
        Sets the status for the course_run to complete
        """
        course_run_grade_status, _ = cls.objects.get_or_create(course_run=course_run)
        if course_run_grade_status.status != FinalGradeStatus.COMPLETE:
            course_run_grade_status.status = FinalGradeStatus.COMPLETE
            course_run_grade_status.save()
        return course_run

    @classmethod
    def create_pending(cls, course_run):
        """
        Creates an entry with status pending
        """

        return course_fg_info

class ProctoredExamGrade(TimestampedModel, AuditableModel):
    """
    Model to store proctored exam grades (like the pearson exams)
    """
    # relationship to other models
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    exam_run = models.ForeignKey(ExamRun, null=True, on_delete=models.CASCADE)

    # fields from proctorate exam results
    exam_date = models.DateTimeField()
    passing_score = models.FloatField()
    score = models.FloatField()
    grade = models.TextField()
    client_authorization_id = models.TextField(null=True, blank=True)

    # dump of all the remaining fields coming from the proctorate exam results
    row_data = JSONField(null=False)

    # custom fields based on proctorate exam results
    passed = models.BooleanField()
    percentage_grade = models.FloatField(null=False)

    @classmethod
    def get_audit_class(cls):
        return ProctoredExamGradeAudit

    @classmethod
    def for_user_course(cls, user, course):
        """
        Returns a queryset of the exam result for an user in a course of a program
        """
        now = now_in_utc()
        return cls.objects.filter(user=user, course=course, exam_run__date_grades_available__lte=now)

    def set_score(self, new_score):
        """
        Sets a value for the score field, then sets related fields based on that value

        Args:
            new_score (float): The new score value
        """
        self.score = new_score
        self.percentage_grade = self.score / 100.0
        self.passed = self.score >= self.passing_score
        self.grade = EXAM_GRADE_PASS if self.passed else EXAM_GRADE_FAIL

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Proctored Exam Grade in course "{course_title}", user "{user}"'.format(
            user=self.user.username,
            course_title=self.course.title
        )

class ProctoredExamGradeAudit(AuditModel):
    """
    Audit table for the ProctoredExamGrade
    """
    proctored_exam_grade = models.ForeignKey(ProctoredExamGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'proctored_exam_grade'

    def __str__(self):
        return 'Proctored Exam Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.proctored_exam_grade.user,
            course_title=self.proctored_exam_grade.course.title
        )

class CombinedFinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store a combined grade for course_run and an exam
    """
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]
    )

    class Meta:
        unique_together = ('user', 'course')

    @classmethod
    def get_audit_class(cls):
        return CombinedFinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Combined Final Grade in course "{course_title}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_title=self.course.title
        )

class CombinedFinalGradeAudit(AuditModel):
    """
    Audit table for CombinedFinalGrade
    """
    combined_final_grade = models.ForeignKey(CombinedFinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'combined_final_grade'

    def __str__(self):
        return 'Combined Final Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.combined_final_grade.user,
            course_title=self.combined_final_grade.course.title
        )


"""
Models for the grades app
"""
import uuid
from django.contrib.auth.models import User
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.utils import IntegrityError

from courses.models import (
    Course,
    CourseRun,
    Program,
)
from exams.models import ExamRun
from exams.constants import EXAM_GRADE_PASS, EXAM_GRADE_FAIL
from grades.constants import FinalGradeStatus
from micromasters.models import (
    AuditableModel,
    AuditModel,
    TimestampedModel,
)
from micromasters.utils import (
    now_in_utc,
    serialize_model_object,
)

class CourseRunGradingAlreadyCompleteError(Exception):
    """
    Custom Exception to raise in case a change of status for a course run is attempted
    """

class FinalGradeQuerySet(models.QuerySet):
    """
    QuerySet class defining common query parameters for FinalGrade
    """
    def passed(self):
        """
        Returns a queryset with a filter that will only fetch passed course runs
        """
        return self.filter(passed=True)

    def paid_on_edx(self):
        """
        Returns a queryset with a filter that will only fetch course runs that are paid on edX.
        Note that a course run can still be paid via an Order if this value is False for a given user.
        """
        return self.filter(course_run_paid_on_edx=True)

    def for_course_run_key(self, edx_course_key):
        """
        Returns a queryset with a filter that will only fetch course runs that match a course key
        """
        return self.filter(course_run__edx_course_key=edx_course_key)

    def for_course_run_keys(self, edx_course_keys):
        """
        Returns a queryset with a filter that will only fetch course runs that match one of many course keys
        """
        return self.filter(course_run__edx_course_key__in=edx_course_keys)

class FinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store edx final grades
    """
    # Set a custom manager for this model. This lets us do a couple useful things:
    # (1) Apply commonly-needed filters by a short, easily-recognized name, and (2) compose a query piece-by-piece.
    objects = FinalGradeQuerySet.as_manager()

    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course_run = models.ForeignKey(CourseRun, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        null=True,
        validators=[MinValueValidator(0.0)]
    )
    passed = models.BooleanField(default=False)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,
    )
    course_run_paid_on_edx = models.BooleanField(default=False)

    class Meta:
        unique_together = ('user', 'course_run')

    @classmethod
    def get_audit_class(cls):
        return FinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    @property
    def grade_percent(self):
        """Returns the grade field value as a number out of 100 (or None if the value is None)"""
        return self.grade * 100 if self.grade is not None else None

    @classmethod
    def get_frozen_users(cls, course_run):
        """
        Returns all the users with a final grade in a given course run
        """
        return list(
            cls.objects.filter(
                course_run=course_run, status=FinalGradeStatus.COMPLETE
            ).values_list('user', flat=True)
        )

    def __str__(self):
        return 'Grade in course "{course_id}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_id=self.course_run.edx_course_key
        )

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden method to run validation"""
        self.full_clean()
        return super().save(*args, **kwargs)

class FinalGradeAudit(AuditModel):
    """
    Audit table for the Final Grade
    """
    final_grade = models.ForeignKey(FinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'final_grade'

    def __str__(self):
        return 'Grade audit for user "{user}", course "{course_id}"'.format(
            user=self.final_grade.user,
            course_id=self.final_grade.course_run.edx_course_key
        )

class MicromastersCourseCertificate(TimestampedModel):
    """
    Model for storing MicroMasters course certificates
    """
    user = models.ForeignKey(User, models.SET_NULL, null=True, related_name='course_certificates')
    course = models.ForeignKey(Course, models.SET_NULL, null=True)
    hash = models.CharField(max_length=32, null=False, unique=True)

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Course Certificate for user={user}, course={course}, hash="{hash}"'.format(
            user=self.user,
            course=self.course.id,
            hash=self.hash,
        )

class MicromastersProgramCertificate(TimestampedModel):
    """
    Model for storing MicroMasters program certificates
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    hash = models.CharField(max_length=32, null=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """Overridden save method"""
        if not self.hash:
            self.hash = uuid.uuid4().hex
        super().save(*args, **kwargs)

    def __str__(self):
        return 'Program Certificate for user={user}, program={program}, hash="{hash}"'.format(
            user=self.user,
            program=self.program,
            hash=self.hash,
        )

class MicromastersProgramCommendation(TimestampedModel):
    """
    Model for storing MicroMasters program congratulation letters
    """
    program = models.ForeignKey(Program, null=False, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        unique_together = ('user', 'program')

    def __str__(self):
        return 'Program letter for user={user}, program={program}, uuid="{uuid}"'.format(
            user=self.user,
            program=self.program,
            uuid=self.uuid,
        )

class CourseRunGradingStatus(TimestampedModel):
    """
    Additional information for the course run related to the final grades
    """
    course_run = models.OneToOneField(CourseRun, null=False, on_delete=models.CASCADE)
    status = models.CharField(
        null=False,
        choices=[(status, status) for status in FinalGradeStatus.ALL_STATUSES],
        default=FinalGradeStatus.PENDING,
        max_length=30,

    )

    def __str__(self):
        return 'Freezing status "{status}" for course "{course_id}"'.format(
            course_id=self.course_run.edx_course_key,
            status=self.status
        )

    @classmethod
    def is_complete(cls, course_run):
        """
        Returns True if there is an entry with status 'complete'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.COMPLETE).exists()

    @classmethod
    def is_pending(cls, course_run):
        """
        Returns True if there is an entry with status 'pending'
        """
        return cls.objects.filter(course_run=course_run, status=FinalGradeStatus.PENDING).exists()

    @classmethod
    def set_to_complete(cls, course_run):
        """
        Sets the status for the course_run to complete
        """
        course_run_grade_status, _ = cls.objects.get_or_create(course_run=course_run)
        if course_run_grade_status.status != FinalGradeStatus.COMPLETE:
            course_run_grade_status.status = FinalGradeStatus.COMPLETE
            course_run_grade_status.save()
        return course_run

    @classmethod
    def create_pending(cls, course_run):
        """
        Creates an entry with status pending
        """
        try:
            course_fg_info, _ = cls.objects.get_or_create(course_run=course_run, status=FinalGradeStatus.PENDING)
        except IntegrityError:
            raise CourseRunGradingAlreadyCompleteError(
                'Course Run "{0}" has already been completed'.format(course_run.edx_course_key))
        return course_fg_info

class ProctoredExamGrade(TimestampedModel, AuditableModel):
    """
    Model to store proctored exam grades (like the pearson exams)
    """
    # relationship to other models
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    exam_run = models.ForeignKey(ExamRun, null=True, on_delete=models.CASCADE)

    # fields from proctorate exam results
    exam_date = models.DateTimeField()
    passing_score = models.FloatField()
    score = models.FloatField()
    grade = models.TextField()
    client_authorization_id = models.TextField(null=True, blank=True)

    # dump of all the remaining fields coming from the proctorate exam results
    row_data = JSONField(null=False)

    # custom fields based on proctorate exam results
    passed = models.BooleanField()
    percentage_grade = models.FloatField(null=False)

    @classmethod
    def get_audit_class(cls):
        return ProctoredExamGradeAudit

    @classmethod
    def for_user_course(cls, user, course):
        """
        Returns a queryset of the exam result for an user in a course of a program
        """
        now = now_in_utc()
        return cls.objects.filter(user=user, course=course, exam_run__date_grades_available__lte=now)

    def set_score(self, new_score):
        """
        Sets a value for the score field, then sets related fields based on that value

        Args:
            new_score (float): The new score value
        """
        self.score = new_score
        self.percentage_grade = self.score / 100.0
        self.passed = self.score >= self.passing_score
        self.grade = EXAM_GRADE_PASS if self.passed else EXAM_GRADE_FAIL

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Proctored Exam Grade in course "{course_title}", user "{user}"'.format(
            user=self.user.username,
            course_title=self.course.title
        )

class ProctoredExamGradeAudit(AuditModel):
    """
    Audit table for the ProctoredExamGrade
    """
    proctored_exam_grade = models.ForeignKey(ProctoredExamGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'proctored_exam_grade'

    def __str__(self):
        return 'Proctored Exam Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.proctored_exam_grade.user,
            course_title=self.proctored_exam_grade.course.title
        )

class CombinedFinalGrade(TimestampedModel, AuditableModel):
    """
    Model to store a combined grade for course_run and an exam
    """
    user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, null=False, on_delete=models.CASCADE)
    grade = models.FloatField(
        validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]
    )

    class Meta:
        unique_together = ('user', 'course')

    @classmethod
    def get_audit_class(cls):
        return CombinedFinalGradeAudit

    def to_dict(self):
        return serialize_model_object(self)

    def __str__(self):
        return 'Combined Final Grade in course "{course_title}", user "{user}", value {grade}'.format(
            user=self.user.username,
            grade=self.grade,
            course_title=self.course.title
        )

class CombinedFinalGradeAudit(AuditModel):
    """
    Audit table for CombinedFinalGrade
    """
    combined_final_grade = models.ForeignKey(CombinedFinalGrade, null=True, on_delete=models.SET_NULL)

    @classmethod
    def get_related_field_name(cls):
        return 'combined_final_grade'

    def __str__(self):
        return 'Combined Final Grade audit for user "{user}", course "{course_title}"'.format(
            user=self.combined_final_grade.user,
            course_title=self.combined_final_grade.course.title
        )


"""Methods for advection-based extrapolation of precipitation fields.
Currently t

he module contains an implementation of the
semi-Lagrangian method described in :cite:`GZ2002` and the
eulerian persistence."""

from pysteps.extrapolation.interface import get_method


if temp%i == 0:
        result.append(i)
        temp = temp//i
    else:
        i += 1

"""
用户输入小于1000的整数
对其进行因式分解
"""

x = int(input("输入小于1000的整数"))
temp = x
i = 2
result = []
while True:
    if temp == 1:
        break

print(x, '=', '*'.join(map(str, result)))


while True:
    if temp == 1:
        break
    if temp%i == 0:
        result.append(i)
        temp = temp//i
    else:
        i += 1

"""
用户输入小于1000的整数
对其进行因式分解
"""

x = int(input("输入小于1000的整数"))
temp = x
i = 2
result = []


"""
用户输入小于1000的整数
对其进

行因式分解
"""

x = int(input("输入小于1000的整数"))
temp = x
i = 2
result = []
while True:
    if temp == 1:
        break
    if temp%i == 0:
        result.append(i)
        temp = temp//i
    else:
        i += 1
print(x, '=', '*'.join(map(str, result)))


"""Test GatewayClient"""
import os
import json
import uuid
from datetime import datetime
from io import StringIO
from unittest.mock import patch

from tornado import gen
from tornado.web import HTTPError
from tornado.httpclient import HTTPRequest, HTTPResponse

from notebook.gateway.managers import GatewayClient
from notebook.utils import maybe_future
from .launchnotebook import NotebookTestBase

def generate_kernelspec(name):
    argv_stanza = ['python', '-m', 'ipykernel_launcher', '-f', '{connection_file}']
    spec_stanza = {'spec': {'argv': argv_stanza, 'env': {}, 'display_name': name, 'language': 'python', 'interrupt_mode': 'signal', 'metadata': {}}}
    kernelspec_stanza = {'name': name, 'spec': spec_stanza, 'resources': {}}
    return kernelspec_stanza

# We'll mock up two kernelspecs - kspec_foo and kspec_bar
kernelspecs = {'default': 'kspec_foo', 'kernelspecs': {'kspec_foo': generate_kernelspec('kspec_foo'), 'kspec_bar': generate_kernelspec('kspec_bar')}}

# maintain a dictionary of expected running kernels.  Key = kernel_id, Value = model.
running_kernels = dict()

"""Generate a mocked kernel model.  Caller is responsible for adding model to running_kernels dictionary."""
    dt = datetime.utcnow().isoformat() + 'Z'
    kernel_id = str(uuid.uuid4())
    model = {'id': kernel_id, 'name': name, 'last_activity': str(dt), 'execution_state': 'idle', 'connections': 1}
    return model

async def mock_gateway_request(url, **kwargs):
    method = 'GET'
    if kwargs['method']:
        method = kwargs['method']

    request = HTTPRequest(url=url, **kwargs)

    endpoint = str(url)

    # Fetch all kernelspecs
    if endpoint.endswith('/api/kernelspecs') and method == 'GET':
        response_buf = StringIO(json.dumps(kernelspecs))
        response = await maybe_future(HTTPResponse(request, 200, buffer=response_buf))
        return response

    # Fetch named kernelspec
    if endpoint.rfind('/api/kernelspecs/') >= 0 and method == 'GET':
        requested_kernelspec = endpoint.rpartition('/')[2]
        kspecs = kernelspecs.get('kernelspecs')
        if requested_kernelspec in kspecs:
            response_buf = StringIO(json.dumps(kspecs.get(requested_kernelspec)))
            response = await maybe_future(HTTPResponse(request, 200, buffer=response_buf))
            return response
        else:
            raise HTTPError(404, message='Kernelspec does not exist: %s' % requested_kernelspec)

    # Create kernel
    if endpoint.endswith('/api/kernels') and method == 'POST':
        json_body = json.loads(kwargs['body'])
        name = json_body.get('name')
        env = json_body.get('env')
        kspec_name = env.get('KERNEL_KSPEC_NAME')
        assert name == kspec_name   # Ensure that KERNEL_ env values get propagated
        model = generate_model(name)
        running_kernels[model.get('id')] = model  # Register model as a running kernel
        response_buf = StringIO(json.dumps(model))
        response = await maybe_future(HTTPResponse(request, 201, buffer=response_buf))
        return response

    # Fetch list of running kernels
    if endpoint.endswith('/api/kernels') and method == 'GET':
        kernels = []
        for kernel_id in running_kernels.keys():
            model = running_kernels.get(kernel_id)
            kernels.append(model)
        response_buf = StringIO(json.dumps(kernels))
        response = await maybe_future(HTTPResponse(request, 200, buffer=response_buf))
        return response

    # Interrupt or restart existing kernel
    if endpoint.rfind('/api/kernels/') >= 0 and method == 'POST':
        requested_kernel_id, sep, action = endpoint.rpartition('/api/kernels/')[2].rpartition('/')

        if action == 'interrupt':
            if requested_kernel_id in running_kernels:
                response = await maybe_future(HTTPResponse(request, 204))
                return response
            else:
                raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id)
        elif action == 'restart':
            if requested_kernel_id in running_kernels:
                response_buf = StringIO(json.dumps(running_kernels.get(requested_kernel_id)))
                response = await maybe_future(HTTPResponse(request, 204, buffer=response_buf))
                return response
            else:
                raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id)
        else:
            raise HTTPError(404, message='Bad action detected: %s' % action)

    # Shutdown existing kernel
    if endpoint.rfind('/api/kernels/') >= 0 and method == 'DELETE':
        requested_kernel_id = endpoint.rpartition('/')[2]
        running_kernels.pop(requested_kernel_id)  # Simulate shutdown by removing kernel from running set
        response = await maybe_future(HTTPResponse(request, 204))
        return response

    # Fetch existing kernel
    if endpoint.rfind('/api/kernels/') >= 0 and method == 'GET':
        requested_kernel_id = endpoint.rpartition('/')[2]
        if requested_kernel_id in running_kernels:
            response_buf = StringIO(json.dumps(running_kernels.get(requested_kernel_id)))
            response = await maybe_future(HTTPResponse(request, 200, buffer=response_buf))
            return response
        else:
            raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id)

mocked_gateway = patch('notebook.gateway.managers.gateway_request', mock_gateway_request)

class TestGateway(NotebookTestBase):

    mock_gateway_url = 'http://mock-gateway-server:8889'
    mock_http_user = 'alice'

    @classmethod
    def setup_class(cls):
        GatewayClient.clear_instance()
        super(TestGateway, cls).setup_class()

    @classmethod
    def teardown_class(cls):
        GatewayClient.clear_instance()
        super(TestGateway, cls).teardown_class()

    @classmethod
    def get_patch_env(cls):
        test_env = super(TestGateway, cls).get_patch_env()
        test_env.update({'JUPYTER_GATEWAY_URL': TestGateway.mock_gateway_url,
                         'JUPYTER_GATEWAY_CONNECT_TIMEOUT': '44.4'})
        return test_env

    @classmethod
    def get_argv(cls):
        argv = super(TestGateway, cls).get_argv()
        argv.extend(['--GatewayClient.request_timeout=96.0', '--GatewayClient.http_user=' + TestGateway.mock_http_user])
        return argv

    def setUp(self):
        kwargs = dict()
        GatewayClient.instance().load_connection_args(**kwargs)
        super().setUp()

    def test_gateway_options(self):
        assert self.notebook.gateway_config.gateway_enabled == True
        assert self.notebook.gateway_config.url == TestGateway.mock_gateway_url
        assert self.notebook.gateway_config.http_user == TestGateway.mock_http_user
        assert self.notebook.gateway_config.connect_timeout == self.notebook.gateway_config.connect_timeout
        assert self.notebook.gateway_config.connect_timeout == 44.4
        assert self.notebook.gateway_config.request_timeout == 96.0
        assert os.environ['KERNEL_LAUNCH_TIMEOUT'] == str(96)  # Ensure KLT gets set from request-timeout

    def test_gateway_class_mappings(self):
        # Ensure appropriate class mappings are in place.
        assert self.notebook.kernel_manager_class.__name__ == 'GatewayKernelManager'
        assert self.notebook.session_manager_class.__name__ == 'GatewaySessionManager'
        assert self.notebook.kernel_spec_manager_class.__name__ == 'GatewayKernelSpecManager'

    def test_gateway_get_kernelspecs(self):
        # Validate that kernelspecs come from gateway.
        with mocked_gateway:
            response = self.request('GET', '/api/kernelspecs')
            self.assertEqual(response.status_code, 200)
            content = json.loads(response.content.decode('utf-8'))
            kspecs = content.get('kernelspecs')
            assert len(kspecs) == 2
            assert kspecs.get('kspec_bar').get('name') == 'kspec_bar'

    def test_gateway_get_named_kernelspec(self):
        # Validate that a specific kernelspec can be retrieved from gateway.
        with mocked_gateway:
            response = self.request('GET', '/api/kernelspecs/kspec_foo')
            assert response.status_code == 200
            kspec_foo = json.loads(response.content.decode('utf-8'))
            assert kspec_foo.get('name') == 'kspec_foo'

            response = self.request('GET', '/api/kernelspecs/no_such_spec')
            assert response.status_code == 404

    def test_gateway_session_lifecycle(self):
        # Validate session lifecycle functions; create and delete.

        # create
        session_id, kernel_id = self.create_session('kspec_foo')

        # ensure kernel still considered running
        self.assertTrue(self.is_kernel_running(kernel_id))

        self.interrupt_kernel(kernel_id)

        # ensure kernel still considered running
        self.assertTrue(self.is_kernel_running(kernel_id))

        # restart
        self.restart_kernel(kernel_id)

        # ensure kernel still considered running
        self.assertTrue(self.is_kernel_running(kernel_id))

        self.delete_session(session_id)
        self.assertFalse(self.is_kernel_running(kernel_id))

    def test_gateway_kernel_lifecycle(self):
        # Validate kernel lifecycle functions; create, interrupt, restart and delete.

        # create
        kernel_id = self.create_kernel('kspec_bar')

        # ensure kernel still considered running
        self.assertTrue(self.is_kernel_running(kernel_id))

        self.interrupt_kernel(kernel_id)

        # ensure kernel still considered running
        self.assertTrue(self.is_kernel_running(kernel_id))

        # restart
        self.restart_kernel(kernel_id)

        # ensure kernel still considered running
        self.assertTrue(self.is_kernel_running(kernel_id))

        self.delete_kernel(kernel_id)
        self.assertFalse(self.is_kernel_running(kernel_id))

    def create_session(self, kernel_name):
        """Creates a session for a kernel.  The session is created against the notebook server
           which then uses the gateway for kernel management.
        """
        with mocked_gateway:
            nb_path = os.path.join(self.notebook_dir, 'testgw.ipynb')
            kwargs = dict()
            kwargs['json'] = {'path': nb_path, 'type': 'notebook', 'kernel': {'name': kernel_name}}

            # add a KERNEL_ value to the current env and we'll ensure that that value exists in the mocked method
            os.environ['KERNEL_KSPEC_NAME'] = kernel_name

            # Create the kernel... (also tests get_kernel)
            response = self.request('POST', '/api/sessions', **kwargs)
            self.assertEqual(response.status_code, 201)
            model = json.loads(response.content.decode('utf-8'))
            self.assertEqual(model.get('path'), nb_path)
            kernel_id = model.get('kernel').get('id')
            # ensure its in the running_kernels and name matches.
            running_kernel = running_kernels.get(kernel_id)
            self.assertEqual(kernel_id, running_kernel.get('id'))
            self.assertEqual(model.get('kernel').get('name'), running_kernel.get('name'))
            session_id = model.get('id')

            # restore env
            os.environ.pop('KERNEL_KSPEC_NAME')
            return session_id, kernel_id

    def delete_session(self, session_id):
        """Deletes a session corresponding to the given session id.
        """
        with mocked_gateway:
            # Delete the session (and kernel)
            response = self.request('DELETE', '/api/sessions/' + session_id)
            self.assertEqual(response.status_code, 204)
            self.assertEqual(response.reason, 'No Content')

    def is_kernel_running(self, kernel_id):
        """Issues request to get the set of running kernels
        """
        with mocked_gateway:
            # Get list of running kernels
            response = self.request('GET', '/api/kernels')
            self.assertEqual(response.status_code, 200)
            kernels = json.loads(response.content.decode('utf-8'))
            self.assertEqual(len(kernels), len(running_kernels))
            for model in kernels:
                if model.get('id') == kernel_id:
                    return True
            return False

    def create_kernel(self, kernel_name):
        """Issues request to restart the given kernel
        """
        with mocked_gateway:
            kwargs = dict()
            kwargs['json'] = {'name': kernel_name}

            # add a KERNEL_ value to the current env and we'll ensure that that value exists in the mocked method
            os.environ['KERNEL_KSPEC_NAME'] = kernel_name

            response = self.request('POST', '/api/kernels', **kwargs)
            self.assertEqual(response.status_code, 201)
            model = json.loads(response.content.decode('utf-8'))
            kernel_id = model.get('id')
            # ensure its in the running_kernels and name matches.
            running_kernel = running_kernels.get(kernel_id)
            self.assertEqual(kernel_id, running_kernel.get('id'))
            self.assertEqual(model.get('name'), kernel_name)

            # restore env
            os.environ.pop('KERNEL_KSPEC_NAME')
            return kernel_id

    def interrupt_kernel(self, kernel_id):
        """Issues request to interrupt the given kernel
        """
        with mocked_gateway:
            response = self.request('POST', '/api/kernels/' + kernel_id + '/interrupt')
            self.assertEqual(response.status_code, 204)
            self.assertEqual(response.reason, 'No Content')

    def restart_kernel(self, kernel_id):
        """Issues request to restart the given kernel
        """
        with mocked_gateway:
            response = self.request('POST', '/api/kernels/' + kernel_id + '/restart')
            self.assertEqual(response.status_code, 200)
            model = json.loads(response.content.decode('utf-8'))
            restarted_kernel_id = model.get('id')
            # ensure its in the running_kernels and name matches.
            running_kernel = running_kernels.get(restarted_kernel_id)
            self.assertEqual(restarted_kernel_id, running_kernel.get('id'))
            self.assertEqual(model.get('name'), running_kernel.get('name'))

    def delete_kernel(self, kernel_id):
        """Deletes kernel corresponding to the given kernel id.
        """
        with mocked_gateway:
            # Delete the session (and kernel)
            response = self.request('DELETE', '/api/kernels/' + kernel_id)
            self.assertEqual(response.status_code, 204)
            self.assertEqual(response.reason, 'No Content')