ser_dict = getattr(espei.refdata, ser_ref_state, {})
    fallback_ser_ref_state = fallback_ref_state + "SER"
    fallback_ser_dict = getattr(espei.refdata, fallback_ser_ref_state)

"""Provides utilities for creating and working with Databases in ESPEI
"""

import logging
from typing import Dict, Union
import sympy
from pycalphad import Database, variables as v
import espei.refdata
from espei.utils import extract_aliases

_log = logging.getLogger(__name__)

def _get_ser_data(element, ref_state, fallback_ref_state="SGTE91") -> Dict[str, Union[str, float]]:
    """Return a dictionary of the stable element reference (SER) data.

    If no SER data is found, returns an empty dictionary.
    """
    ser_ref_state = ref_state + "SER"
    # Return an empty dict for backwards compatibility, since the SER data may not exist

    el_ser_data = ser_dict.get(element)
    if el_ser_data is None and ref_state == fallback_ref_state:
        # No data found, no fallback alternative
        _log.warning("%s has no entry in the %s reference data. Fitting formation energies will not be possible.", element, ser_ref_state)
    elif el_ser_data is None:
        # No data found, try the fallback
        el_ser_data = fallback_ser_dict.get(element)
        if el_ser_data is None:
            # No data found in the fallback
            _log.warning("%s has no entry in the %s reference data nor in the %s fallback reference data. Fitting formation energies will not be possible.", element, ser_ref_state + "SER", fallback_ser_ref_state)
            return {}
        else:
            # Data found in the fallback
            _log.trace("%s has no entry in the %s reference data, but was available in the %s fallback reference data.", element, ser_ref_state + "SER", fallback_ser_ref_state)
    if el_ser_data is not None:
        return el_ser_data
    else:
        return {}

def initialize_database(phase_models, ref_state, dbf=None, fallback_ref_state="SGTE91"):
    """Return a Database boostraped with elements, species, phases and unary lattice stabilities.

    Parameters
    ----------
    phase_models : Dict[str, Any]
        Dictionary of components and phases to fit.
    ref_state : str
        String of the reference data to use, e.g. 'SGTE91' or 'SR2016'
    dbf : Optional[Database]
        Initial pycalphad Database that can have parameters that would not be fit by ESPEI
    fallback_ref_state : str
        String of the reference data to use for SER data, defaults to 'SGTE91'

    Returns
    -------
    Database
        A new pycalphad Database object, or a modified one if it was given.

    """
    if dbf is None:
        dbf = Database()
    lattice_stabilities = getattr(espei.refdata, ref_state)
    ser_stability = getattr(espei.refdata, ref_state + "Stable")
    aliases = extract_aliases(phase_models)
    phases = sorted({ph.upper() for ph in phase_models["phases"].keys()})
    elements = {el.upper() for el in phase_models["components"]}
    dbf.elements.update(elements)
    dbf.species.update({v.Species(el, {el: 1}, 0) for el in elements})

    # Add SER reference data for this element
    for element in dbf.elements:
        if element in dbf.refstates:
            continue  # Do not clobber user reference states
        el_ser_data = _get_ser_data(element, ref_state, fallback_ref_state=fallback_ref_state)
        # Try to look up the alias that we are using in this fitting
        el_ser_data["phase"] = aliases.get(el_ser_data["phase"], el_ser_data["phase"])
        # Don't warn if the element is a species with no atoms because per-atom
        # formation energies are not possible (e.g. VA (VACUUM) or /- (ELECTRON_GAS))
        if el_ser_data["phase"] not in phases and v.Species(element).number_of_atoms != 0:
            # We have the Gibbs energy expression that we need in the reference
            # data, but this phase is not a candidate in the phase models. The
            # phase won't be added to the database, so looking up the phases's
            # energy won't work.
            _log.warning(
                "The reference phase for %s, %s, is not in the supplied phase models "
                "and won't be added to the Database phases. Fitting formation "
                "energies will not be possible.", element, el_ser_data["phase"]
            )
        dbf.refstates[element] = el_ser_data

    # Add the phases
    for phase_name, phase_data in phase_models['phases'].items():
        if phase_name not in dbf.phases.keys():  # Do not clobber user phases
            # TODO: Need to support model hints for: magnetic, order-disorder, etc.
            site_ratios = phase_data['sublattice_site_ratios']
            subl_model = phase_data['sublattice_model']
            # Only generate the sublattice model for active components
            subl_model = [sorted(set(subl).intersection(dbf.elements)) for subl in subl_model]
            if all(len(subl) > 0 for subl in subl_model):
                dbf.add_phase(phase_name, dict(), site_ratios)
                dbf.add_phase_constituents(phase_name, subl_model)

    # Add the GHSER functions to the Database
    for element in dbf.elements:
        # Use setdefault here to not clobber user-provided functions
        if element == "VA":
            dbf.symbols.setdefault("GHSERVA", 0)
        else:
            # note that `c.upper()*2)[:2]` returns "AL" for "Al" and "BB" for "B"
            # Using this ensures that GHSER functions will be unique, e.g.
            # GHSERC would be an abbreviation for GHSERCA.
            sym_name = "GHSER" + (element.upper()*2)[:2]
            dbf.symbols.setdefault(sym_name, ser_stability[element])
    return dbf


el_ser_data = ser_dict.get(element)
    if el_ser_data is None and ref_state == fallback_ref_state:
        # No data found, no fallback alternative
        _log.warning("%s has no entry in the %s reference data. Fitting formation energies will not be possible.", element, ser_ref_state)
    elif el_ser_data is None:
        # No data found, try the fallback
        el_ser_data = fallback_ser_dict.get(element)
        if el_ser_data is None:
            # No data found in the fallback
            _log.warning("%s has no entry in the %s reference data nor in the %s fallback reference data. Fitting formation energies will not be possible.", element, ser_ref_state + "SER", fallback_ser_ref_state)
            return {}
        else:
            # Data found in the fallback
            _log.trace("%s has no entry in the %s reference data, but was available in the %s fallback reference data.", element, ser_ref_state + "SER", fallback_ser_ref_state)
    if el_ser_data is not None:
        return el_ser_data
    else:
        return {}

"""Provides utilities for creating and working with Databases in ESPEI
"""

import logging
from typing import Dict, Union
import sympy
from pycalphad import Database, variables as v
import espei.refdata
from espei.utils import extract_aliases

_log = logging.getLogger(__name__)

def _get_ser_data(element, ref_state, fallback_ref_state="SGTE91") -> Dict[str, Union[str, float]]:
    """Return a dictionary of the stable element reference (SER) data.

    If no SER data is found, returns an empty dictionary.
    """
    ser_ref_state = ref_state + "SER"
    # Return an empty dict for backwards compatibility, since the SER data may not exist
    ser_dict = getattr(espei.refdata, ser_ref_state, {})
    fallback_ser_ref_state = fallback_ref_state + "SER"
    fallback_ser_dict = getattr(espei.refdata, fallback_ser_ref_state)

def initialize_database(phase_models, ref_state, dbf=None, fallback_ref_state="SGTE91"):
    """Return a Database boostraped with elements, species, phases and unary lattice stabilities.

    Parameters
    ----------
    phase_models : Dict[str, Any]
        Dictionary of components and phases to fit.
    ref_state : str
        String of the reference data to use, e.g. 'SGTE91' or 'SR2016'
    dbf : Optional[Database]
        Initial pycalphad Database that can have parameters that would not be fit by ESPEI
    fallback_ref_state : str
        String of the reference data to use for SER data, defaults to 'SGTE91'

    Returns
    -------
    Database
        A new pycalphad Database object, or a modified one if it was given.

    """
    if dbf is None:
        dbf = Database()
    lattice_stabilities = getattr(espei.refdata, ref_state)
    ser_stability = getattr(espei.refdata, ref_state + "Stable")
    aliases = extract_aliases(phase_models)
    phases = sorted({ph.upper() for ph in phase_models["phases"].keys()})
    elements = {el.upper() for el in phase_models["components"]}
    dbf.elements.update(elements)
    dbf.species.update({v.Species(el, {el: 1}, 0) for el in elements})

    # Add SER reference data for this element
    for element in dbf.elements:
        if element in dbf.refstates:
            continue  # Do not clobber user reference states
        el_ser_data = _get_ser_data(element, ref_state, fallback_ref_state=fallback_ref_state)
        # Try to look up the alias that we are using in this fitting
        el_ser_data["phase"] = aliases.get(el_ser_data["phase"], el_ser_data["phase"])
        # Don't warn if the element is a species with no atoms because per-atom
        # formation energies are not possible (e.g. VA (VACUUM) or /- (ELECTRON_GAS))
        if el_ser_data["phase"] not in phases and v.Species(element).number_of_atoms != 0:
            # We have the Gibbs energy expression that we need in the reference
            # data, but this phase is not a candidate in the phase models. The
            # phase won't be added to the database, so looking up the phases's
            # energy won't work.
            _log.warning(
                "The reference phase for %s, %s, is not in the supplied phase models "
                "and won't be added to the Database phases. Fitting formation "
                "energies will not be possible.", element, el_ser_data["phase"]
            )
        dbf.refstates[element] = el_ser_data

    # Add the phases
    for phase_name, phase_data in phase_models['phases'].items():
        if phase_name not in dbf.phases.keys():  # Do not clobber user phases
            # TODO: Need to support model hints for: magnetic, order-disorder, etc.
            site_ratios = phase_data['sublattice_site_ratios']
            subl_model = phase_data['sublattice_model']
            # Only generate the sublattice model for active components
            subl_model = [sorted(set(subl).intersection(dbf.elements)) for subl in subl_model]
            if all(len(subl) > 0 for subl in subl_model):
                dbf.add_phase(phase_name, dict(), site_ratios)
                dbf.add_phase_constituents(phase_name, subl_model)

    # Add the GHSER functions to the Database
    for element in dbf.elements:
        # Use setdefault here to not clobber user-provided functions
        if element == "VA":
            dbf.symbols.setdefault("GHSERVA", 0)
        else:
            # note that `c.upper()*2)[:2]` returns "AL" for "Al" and "BB" for "B"
            # Using this ensures that GHSER functions will be unique, e.g.
            # GHSERC would be an abbreviation for GHSERCA.
            sym_name = "GHSER" + (element.upper()*2)[:2]
            dbf.symbols.setdefault(sym_name, ser_stability[element])
    return dbf


) for ph in phase_models["phases"].keys()})

"""Provides utilities for creating and working with Databases in ESPEI
"""

import logging
from typing import Dict, Union
import sympy
from pycalphad import Database, variables as v
import espei.refdata
from espei.utils import extract_aliases

_log = logging.getLogger(__name__)

def _get_ser_data(element, ref_state, fallback_ref_state="SGTE91") -> Dict[str, Union[str, float]]:
    """Return a dictionary of the stable element reference (SER) data.

    If no SER data is found, returns an empty dictionary.
    """
    ser_ref_state = ref_state + "SER"
    # Return an empty dict for backwards compatibility, since the SER data may not exist
    ser_dict = getattr(espei.refdata, ser_ref_state, {})
    fallback_ser_ref_state = fallback_ref_state + "SER"
    fallback_ser_dict = getattr(espei.refdata, fallback_ser_ref_state)
    el_ser_data = ser_dict.get(element)
    if el_ser_data is None and ref_state == fallback_ref_state:
        # No data found, no fallback alternative
        _log.warning("%s has no entry in the %s reference data. Fitting formation energies will not be possible.", element, ser_ref_state)
    elif el_ser_data is None:
        # No data found, try the fallback
        el_ser_data = fallback_ser_dict.get(element)
        if el_ser_data is None:
            # No data found in the fallback
            _log.warning("%s has no entry in the %s reference data nor in the %s fallback reference data. Fitting formation energies will not be possible.", element, ser_ref_state + "SER", fallback_ser_ref_state)
            return {}
        else:
            # Data found in the fallback
            _log.trace("%s has no entry in the %s reference data, but was available in the %s fallback reference data.", element, ser_ref_state + "SER", fallback_ser_ref_state)
    if el_ser_data is not None:
        return el_ser_data
    else:
        return {}

def initialize_database(phase_models, ref_state, dbf=None, fallback_ref_state="SGTE91"):
    """Return a Database boostraped with elements, species, phases and unary lattice stabilities.

    Parameters
    ----------
    phase_models : Dict[str, Any]
        Dictionary of components and phases to fit.
    ref_state : str
        String of the reference data to use, e.g. 'SGTE91' or 'SR2016'
    dbf : Optional[Database]
        Initial pycalphad Database that can have parameters that would not be fit by ESPEI
    fallback_ref_state : str
        String of the reference data to use for SER data, defaults to 'SGTE91'

    Returns
    -------
    Database
        A new pycalphad Database object, or a modified one if it was given.

    """
    if dbf is None:
        dbf = Database()
    lattice_stabilities = getattr(espei.refdata, ref_state)
    ser_stability = getattr(espei.refdata, ref_state + "Stable")
    aliases = extract_aliases(phase_models)
    phases = sorted({ph.upper(

    elements = {el.upper() for el in phase_models["components"]}
    dbf.elements.update(elements)
    dbf.species.update({v.Species(el, {el: 1}, 0) for el in elements})

    # Add SER reference data for this element
    for element in dbf.elements:
        if element in dbf.refstates:
            continue  # Do not clobber user reference states
        el_ser_data = _get_ser_data(element, ref_state, fallback_ref_state=fallback_ref_state)
        # Try to look up the alias that we are using in this fitting
        el_ser_data["phase"] = aliases.get(el_ser_data["phase"], el_ser_data["phase"])
        # Don't warn if the element is a species with no atoms because per-atom
        # formation energies are not possible (e.g. VA (VACUUM) or /- (ELECTRON_GAS))
        if el_ser_data["phase"] not in phases and v.Species(element).number_of_atoms != 0:
            # We have the Gibbs energy expression that we need in the reference
            # data, but this phase is not a candidate in the phase models. The
            # phase won't be added to the database, so looking up the phases's
            # energy won't work.
            _log.warning(
                "The reference phase for %s, %s, is not in the supplied phase models "
                "and won't be added to the Database phases. Fitting formation "
                "energies will not be possible.", element, el_ser_data["phase"]
            )
        dbf.refstates[element] = el_ser_data

    # Add the phases
    for phase_name, phase_data in phase_models['phases'].items():
        if phase_name not in dbf.phases.keys():  # Do not clobber user phases
            # TODO: Need to support model hints for: magnetic, order-disorder, etc.
            site_ratios = phase_data['sublattice_site_ratios']
            subl_model = phase_data['sublattice_model']
            # Only generate the sublattice model for active components
            subl_model = [sorted(set(subl).intersection(dbf.elements)) for subl in subl_model]
            if all(len(subl) > 0 for subl in subl_model):
                dbf.add_phase(phase_name, dict(), site_ratios)
                dbf.add_phase_constituents(phase_name, subl_model)

    # Add the GHSER functions to the Database
    for element in dbf.elements:
        # Use setdefault here to not clobber user-provided functions
        if element == "VA":
            dbf.symbols.setdefault("GHSERVA", 0)
        else:
            # note that `c.upper()*2)[:2]` returns "AL" for "Al" and "BB" for "B"
            # Using this ensures that GHSER functions will be unique, e.g.
            # GHSERC would be an abbreviation for GHSERCA.
            sym_name = "GHSER" + (element.upper()*2)[:2]
            dbf.symbols.setdefault(sym_name, ser_stability[element])
    return dbf


if el_ser_data is not None:
        return el_ser_data
    else:
        return {}

"""Provides utilities for creating and working with Databases in ESPEI
"""

import logging
from typing import Dict, Union
import sympy
from pycalphad import Database, variables as v
import espei.refdata
from espei.utils import extract_aliases

_log = logging.getLogger(__name__)

def _get_ser_data(element, ref_state, fallback_ref_state="SGTE91") -> Dict[str, Union[str, float]]:
    """Return a dictionary of the stable element reference (SER) data.

    If no SER data is found, returns an empty dictionary.
    """
    ser_ref_state = ref_state + "SER"
    # Return an empty dict for backwards compatibility, since the SER data may not exist
    ser_dict = getattr(espei.refdata, ser_ref_state, {})
    fallback_ser_ref_state = fallback_ref_state + "SER"
    fallback_ser_dict = getattr(espei.refdata, fallback_ser_ref_state)
    el_ser_data = ser_dict.get(element)
    if el_ser_data is None and ref_state == fallback_ref_state:
        # No data found, no fallback alternative
        _log.warning("%s has no entry in the %s reference data. Fitting formation energies will not be possible.", element, ser_ref_state)
    elif el_ser_data is None:
        # No data found, try the fallback
        el_ser_data = fallback_ser_dict.get(element)
        if el_ser_data is None:
            # No data found in the fallback
            _log.warning("%s has no entry in the %s reference data nor in the %s fallback reference data. Fitting formation energies will not be possible.", element, ser_ref_state + "SER", fallback_ser_ref_state)
            return {}
        else:
            # Data found in the fallback
            _log.trace("%s has no entry in the %s reference data, but was available in the %s fallback reference data.", element, ser_ref_state + "SER", fallback_ser_ref_state)


"""Provides utilities for creating and working with Databases in ESPEI
"""

import logging
from typing import Dict, Union
import sympy
from pycalphad import Database, variables as v
import espei.refdata
from espei.utils import extract_aliases

_log = logging.getLogger(__name__)

def _get_ser_data(element, ref_state, fallback_ref_state="SGTE91") -> Dict[str, Union[str, float]]:
    """Return a dictionary of the stable element reference (SER) data.

    If no SER data is found, returns an empty dictionary.
    """
    ser_ref_state = ref_state + "SER"
    # Return an empty dict for backwards compatibility, since the SER data may not exist
    ser_dict = getattr(espei.refdata, ser_ref_state, {})
    fallback_ser_ref_state = fallback_ref_state + "SER"
    fallback_ser_dict = getattr(espei.refdata, fallback_ser_ref_state)
    el_ser_data = ser_dict.get(element)
    if el_ser_data is None and ref_state == fallback_ref_state:
        # No data found, no fallback alternative
        _log.warning("%s has no entry in the %s reference data. Fitting formation energies will not be possible.", element, ser_ref_state)
    elif el_ser_data is None:
        # No data found, try the fallback
        el_ser_data = fallback_ser_dict.get(element)
        if el_ser_data is None:
            # No data found in the fallback
            _log.warning("%s has no entry in the %s reference data nor in the %s fallback reference data. Fitting formation energies will not be possible.", element, ser_re

f_state + "SER", fallback_ser_ref_state)
            return {}
        else:
            # Data found in the fallback
            _log.trace("%s has no entry in the %s reference data, but was available in the %s fallback reference data.", element, ser_ref_state + "SER", fallback_ser_ref_state)
    if el_ser_data is not None:
        return el_ser_data
    else:
        return {}

def initialize_database(phase_models, ref_state, dbf=None, fallback_ref_state="SGTE91"):
    """Return a Database boostraped with elements, species, phases and unary lattice stabilities.

    Parameters
    ----------
    phase_models : Dict[str, Any]
        Dictionary of components and phases to fit.
    ref_state : str
        String of the reference data to use, e.g. 'SGTE91' or 'SR2016'
    dbf : Optional[Database]
        Initial pycalphad Database that can have parameters that would not be fit by ESPEI
    fallback_ref_state : str
        String of the reference data to use for SER data, defaults to 'SGTE91'

    Returns
    -------
    Database
        A new pycalphad Database object, or a modified one if it was given.

    """
    if dbf is None:
        dbf = Database()
    lattice_stabilities = getattr(espei.refdata, ref_state)
    ser_stability = getattr(espei.refdata, ref_state + "Stable")
    aliases = extract_aliases(phase_models)
    phases = sorted({ph.upper() for ph in phase_models["phases"].keys()})
    elements = {el.upper() for el in phase_models["components"]}
    dbf.elements.update(elements)
    dbf.species.update({v.Species(el, {el: 1}, 0) for el in elements})

    # Add SER reference data for this element
    for element in dbf.elements:
        if element in dbf.refstates:
            continue  # Do not clobber user reference states
        el_ser_data = _get_ser_data(element, ref_state, fallback_ref_state=fallback_ref_state)
        # Try to look up the alias that we are using in this fitting
        el_ser_data["phase"] = aliases.get(el_ser_data["phase"], el_ser_data["phase"])
        # Don't warn if the element is a species with no atoms because per-atom
        # formation energies are not possible (e.g. VA (VACUUM) or /- (ELECTRON_GAS))
        if el_ser_data["phase"] not in phases and v.Species(element).number_of_atoms != 0:
            # We have the Gibbs energy expression that we need in the reference
            # data, but this phase is not a candidate in the phase models. The
            # phase won't be added to the database, so looking up the phases's
            # energy won't work.
            _log.warning(
                "The reference phase for %s, %s, is not in the supplied phase models "
                "and won't be added to the Database phases. Fitting formation "
                "energies will not be possible.", element, el_ser_data["phase"]
            )
        dbf.refstates[element] = el_ser_data

    # Add the phases
    for phase_name, phase_data in phase_models['phases'].items():
        if phase_name not in dbf.phases.keys():  # Do not clobber user phases
            # TODO: Need to support model hints for: magnetic, order-disorder, etc.
            site_ratios = phase_data['sublattice_site_ratios']
            subl_model = phase_data['sublattice_model']
            # Only generate the sublattice model for active components
            subl_model = [sorted(set(subl).intersection(dbf.elements)) for subl in subl_model]
            if all(len(subl) > 0 for subl in subl_model):
                dbf.add_phase(phase_name, dict(), site_ratios)
                dbf.add_phase_constituents(phase_name, subl_model)

    # Add the GHSER functions to the Database
    for element in dbf.elements:
        # Use setdefault here to not clobber user-provided functions
        if element == "VA":
            dbf.symbols.setdefault("GHSERVA", 0)
        else:
            # note that `c.upper()*2)[:2]` returns "AL" for "Al" and "BB" for "B"
            # Using this ensures that GHSER functions will be unique, e.g.
            # GHSERC would be an abbreviation for GHSERCA.
            sym_name = "GHSER" + (element.upper()*2)[:2]
            dbf.symbols.setdefault(sym_name, ser_stability[element])
    return dbf


def define_optimizer(conf, model):
    # define the param to optimize.

from pcode.optim.sgd import SGD

from pcode.optim.dgc import DGC
from pcode.optim.parallel_choco import ParallelCHOCO
from pcode.optim.parallel_choco_v import ParallelCHOCO_V
from pcode.optim.ef_sign_sgd import EF_SignSGD
from pcode.optim.dcd_psgd import DCD_PSGD
from pcode.optim.ecd_psgd import ECD_PSGD
from pcode.optim.deep_squeeze import DeepSqueeze

params = [
        {
            "params": [value],
            "name": key,
            "weight_decay": conf.weight_decay if "bn" not in key else 0.0,
            "param_size": value.size(),
            "nelement": value.nelement(),
        }
        for key, value in model.named_parameters()
    ]

    # define the optimizer.
    if conf.optimizer == "sgd":
        optim_class = SGD
    elif conf.optimizer == "dgc":
        optim_class = DGC
    elif conf.optimizer == "dcd_psgd":
        optim_class = DCD_PSGD
    elif conf.optimizer == "ecd_psgd":
        optim_class = ECD_PSGD
    elif conf.optimizer == "parallel_choco":
        optim_class = ParallelCHOCO
    elif conf.optimizer == "parallel_choco_v":
        optim_class = ParallelCHOCO_V
    elif conf.optimizer == "ef_sign_sgd":
        optim_class = EF_SignSGD
    elif conf.optimizer == "deep_squeeze":
        optim_class = DeepSqueeze
    else:
        raise NotImplementedError

    return optim_class(
        params,
        lr=conf.lr,
        momentum=conf.momentum_factor,
        nesterov=conf.use_nesterov,
        conf=conf,
    )


params = [
        {
            "params": [value],
            "name": key,
            "weight_decay": conf.weight_decay if "bn" not in key else 0.0,
            "param_size": value.size(),
            "nelement": value.nelement(),
        }
        for key, value in model.named_parameters()
    ]

    # define the optimizer.

from pcode.optim.sgd import SGD

from pcode.optim.dgc import DGC
from pcode.optim.parallel_choco import ParallelCHOCO
from pcode.optim.parallel_choco_v import ParallelCHOCO_V
from pcode.optim.ef_sign_sgd import EF_SignSGD
from pcode.optim.dcd_psgd import DCD_PSGD
from pcode.optim.ecd_psgd import ECD_PSGD
from pcode.optim.deep_squeeze import DeepSqueeze

def define_optimizer(conf, model):
    # define the param to optimize.

    if conf.optimizer == "sgd":
        optim_class = SGD
    elif conf.optimizer == "dgc":
        optim_class = DGC
    elif conf.optimizer == "dcd_psgd":
        optim_class = DCD_PSGD
    elif conf.optimizer == "ecd_psgd":
        optim_class = ECD_PSGD
    elif conf.optimizer == "parallel_choco":
        optim_class = ParallelCHOCO
    elif conf.optimizer == "parallel_choco_v":
        optim_class = ParallelCHOCO_V
    elif conf.optimizer == "ef_sign_sgd":
        optim_class = EF_SignSGD
    elif conf.optimizer == "deep_squeeze":
        optim_class = DeepSqueeze
    else:
        raise NotImplementedError

    return optim_class(
        params,
        lr=conf.lr,
        momentum=conf.momentum_factor,
        nesterov=conf.use_nesterov,
        conf=conf,
    )


if conf.optimizer == "sgd":
        optim_class = SGD
    elif conf.optimizer == "dgc":
        optim_class = DGC
    elif conf.optimizer == "dcd_psgd":
        optim_class = DCD_PSGD
    elif conf.optimizer == "ecd_psgd":
        optim_class = ECD_PSGD
    elif conf.optimizer == "parallel_choco":
        optim_class = ParallelCHOCO
    elif conf.optimizer == "parallel_choco_v":
        optim_class = ParallelCHOCO_V
    elif conf.optimizer == "ef_sign_sgd":
        optim_class = EF_SignSGD
    elif conf.optimizer == "deep_squeeze":
        optim_class = DeepSqueeze
    else:
        raise NotImplementedError

from pcode.optim.sgd import SGD

from pcode.optim.dgc import DGC
from pcode.optim.parallel_choco import ParallelCHOCO
from pcode.optim.parallel_choco_v import ParallelCHOCO_V
from pcode.optim.ef_sign_sgd import EF_SignSGD
from pcode.optim.dcd_psgd import DCD_PSGD
from pcode.optim.ecd_psgd import ECD_PSGD
from pcode.optim.deep_squeeze import DeepSqueeze

def define_optimizer(conf, model):
    # define the param to optimize.
    params = [
        {
            "params": [value],
            "name": key,
            "weight_decay": conf.weight_decay if "bn" not in key else 0.0,
            "param_size": value.size(),
            "nelement": value.nelement(),
        }
        for key, value in model.named_parameters()
    ]

    # define the optimizer.

    return optim_class(
        params,
        lr=conf.lr,
        momentum=conf.momentum_factor,
        nesterov=conf.use_nesterov,
        conf=conf,
    )


return optim_class(
        params,
        lr=conf.lr,
        momentum=conf.momentum_factor,
        nesterov=conf.use_nesterov,
        conf=conf,
    )

from pcode.optim.sgd import SGD

from pcode.optim.dgc import DGC
from pcode.optim.parallel_choco import ParallelCHOCO
from pcode.optim.parallel_choco_v import ParallelCHOCO_V
from pcode.optim.ef_sign_sgd import EF_SignSGD
from pcode.optim.dcd_psgd import DCD_PSGD
from pcode.optim.ecd_psgd import ECD_PSGD
from pcode.optim.deep_squeeze import DeepSqueeze

def define_optimizer(conf, model):
    # define the param to optimize.
    params = [
        {
            "params": [value],
            "name": key,
            "weight_decay": conf.weight_decay if "bn" not in key else 0.0,
            "param_size": value.size(),
            "nelement": value.nelement(),
        }
        for key, value in model.named_parameters()
    ]

    # define the optimizer.
    if conf.optimizer == "sgd":
        optim_class = SGD
    elif conf.optimizer == "dgc":
        optim_class = DGC
    elif conf.optimizer == "dcd_psgd":
        optim_class = DCD_PSGD
    elif conf.optimizer == "ecd_psgd":
        optim_class = ECD_PSGD
    elif conf.optimizer == "parallel_choco":
        optim_class = ParallelCHOCO
    elif conf.optimizer == "parallel_choco_v":
        optim_class = ParallelCHOCO_V
    elif conf.optimizer == "ef_sign_sgd":
        optim_class = EF_SignSGD
    elif conf.optimizer == "deep_squeeze":
        optim_class = DeepSqueeze
    else:
        raise NotImplementedError


from pcode.optim.sgd import SGD

from pcode.optim.dgc import DGC
from pcode.optim.parallel_choco import ParallelCHOCO
from pcode.optim.parallel_choco_v import ParallelCHOCO_V
from pcode.optim.ef_sign_sgd import EF_SignSGD
from pcode.optim.dcd_psgd import DCD_PSGD
from pcode.optim.ecd_psgd import ECD_PSGD
from pcode.optim.deep_squeeze import DeepSqueeze

def define_optimizer(conf, model):
    # define the param to optimize.
    params = [
        {
            "params": [value],
            "name": key,
            "weight_decay": conf.weight_decay if "bn" not in key else 0.0,
            "param_size": value.size(),
            "nelement": value.nelement(),
        }
        for key, value in model.named_parameters()
    ]

    # define the optimizer.
    if conf.optimizer == "sgd":
        optim_class = SGD
    elif conf.optimizer == "dgc":
        optim_class = DGC
    elif conf.optimizer == "dcd_psgd":
        optim_class = DCD_PSGD
    elif conf.optimizer == "ecd_psgd":
        optim_class = ECD_PSGD
    elif conf.optimizer == "parallel_choco":
        optim_class = ParallelCHOCO
    elif conf.optimizer == "parallel_choco_v":
        optim_class = ParallelCHOCO_V
    elif conf.optimizer == "ef_sign_sgd":
        optim_class = EF_SignSGD
    elif conf.optimizer == "deep_squeeze":
        optim_class = DeepSqueeze
    else:
        raise NotImplementedError

    return optim_class(
        params,
        lr=conf.lr,
        momentum=conf.momentum_factor,
        nesterov=conf.use_nesterov


def __init__(self, plotly_name="ycalendar", parent_name="scattergl", **kwargs):

import _plotly_utils.basevalidators

class YcalendarValidator(_plotly_utils.basevalidators.EnumeratedValidator):

super(YcalendarValidator, self).__init__(
            plotly_name=plotly_name,
            parent_name=parent_name,
            edit_type=kwargs.pop("edit_type", "calc"),
            values=kwargs.pop(
                "values",
                [
                    "chinese",
                    "coptic",
                    "discworld",
                    "ethiopian",
                    "gregorian",
                    "hebrew",
                    "islamic",
                    "jalali",
                    "julian",
                    "mayan",
                    "nanakshahi",
                    "nepali",
                    "persian",
                    "taiwan",
                    "thai",
                    "ummalqura",
                ],
            ),
            **kwargs
        )


import _plotly_utils.basevalidators

class YcalendarValidator(_plotly_utils.basevalidators.EnumeratedValidator):
    def __init__(self, plotly_name="ycalendar", parent_name="scattergl", **kwargs):
        super(YcalendarValidator, self).__init__(
            plotly_name=plotly_name,
            parent_name=parent_name,
            edit_type=kwargs.pop("edit_type", "calc"),
            values=kwargs.pop(
                "values",
                [
                    "chinese",
                    "coptic",

           "discworld",
                    "ethiopian",
                    "gregorian",
                    "hebrew",
                    "islamic",
                    "jalali",
                    "julian",
                    "mayan",
                    "nanakshahi",
                    "nepali",
                    "persian",
                    "taiwan",
                    "thai",
                    "ummalqura",
                ],
            ),
            **kwargs
        )


def __init__(self, plotly_name="title", parent_name="layout.yaxis", **kwargs):

import _plotly_utils.basevalidators

class TitleValidator(_plotly_utils.basevalidators.TitleValidator):

super(TitleValidator, self).__init__(
            plotly_name=plotly_name,
            parent_name=parent_name,
            data_class_str=kwargs.pop("data_class_str", "Title"),
            data_docs=kwargs.pop(
                "data_docs",
                """
            font
                Sets this axis' title font. Note that the
                title's font used to be customized by the now
                deprecated `titlefont` attribute.
            standoff
                Sets the standoff distance (in px) between the
                axis labels and the title text The default
                value is a function of the axis tick labels,
                the title `font.size` and the axis `linewidth`.
                Note that the axis title position is always
                constrained within the margins, so the actual
                standoff distance is always less than the set
                or default value. By setting `standoff` and
                turning on `automargin`, plotly.js will push
                the margins to fit the axis title at given
                standoff distance.
            text
                Sets the title of this axis. Note that before
                the existence of `title.text`, the title's
                contents used to be defined as the `title`
                attribute itself. This behavior has been
                deprecated.
""",
            ),
            **kwargs
        )


import _plotly_utils.basevalidators

class TitleValidator(_plotly_utils.basevalidators.TitleValidator):
    def __init__(self, plotly_name="title", parent_name="layout.yaxis", **kwargs):
        super(TitleValidator, self).__init__(
            plotly_name=plotly_name,
            parent_name=parent_name,
            data_class_str=kwargs.pop("data_class_str", "Title"),
            data_docs=kwargs.pop(
                "data_docs",
                """
            font
                Sets this axis' title font. Note that the
                title's font used to be customized by the now
                deprecated `titlefont` attribute.
            standoff
                Sets the standoff distance (in px) between the
                axis labels and the title text The default
                value is a function of the axis tick labels,
                the title `font.size` and the axis `linewidth`.
                Note that the axis title position is always
                constrained within the margins, so the actual
                standoff distance is always less than the set
                or default value. By setting `standoff` and
                turning on `automargin`, plotly.js will push
                the margins to fit the axis title at given
                standoff distance.
            text
                Sets the title of this axis. Note that before
                the existence of `title.text`, the title's
                contents used to be defined as the `title`

attribute itself. This behavior has been
                deprecated.
""",
            ),
            **kwargs
        )


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

"""
        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
        )


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

"""
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):

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
        )


"""
        Sets the status for the course_run to complete
        """
        course_run_grade_status, _ = cls.objects.get_or_create(course_run=course_run)

"""
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):

        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
        )


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
        )