scores.py 3.36 KB
Newer Older
1 2 3 4 5 6 7 8 9
"""
Functionality for problem scores.
"""
from openedx.core.lib.cache_utils import memoized
from xblock.core import XBlock
from .transformer import GradesTransformer


@memoized
10
def block_types_possibly_scored():
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
    """
    Returns the block types that could have a score.

    Something might be a scored item if it is capable of storing a score
    (has_score=True). We also have to include anything that can have children,
    since those children might have scores. We can avoid things like Videos,
    which have state but cannot ever impact someone's grade.
    """
    return frozenset(
        cat for (cat, xblock_class) in XBlock.load_classes() if (
            getattr(xblock_class, 'has_score', False) or getattr(xblock_class, 'has_children', False)
        )
    )


def possibly_scored(usage_key):
    """
    Returns whether the given block could impact grading (i.e. scored, or has children).
    """
30
    return usage_key.block_type in block_types_possibly_scored()
31 32


33
def weighted_score(raw_earned, raw_possible, weight):
34 35
    """Return a tuple that represents the weighted (correct, total) score."""
    # If there is no weighting, or weighting can't be applied, return input.
36 37
    if weight is None or raw_possible == 0:
        return (raw_earned, raw_possible)
38
    return float(raw_earned) * weight / raw_possible, float(weight)
39 40 41 42


def get_score(user, block, scores_client, submissions_scores_cache):
    """
43
    Return the score for a user on a problem, as a tuple (earned, possible).
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
    e.g. (5,7) if you got 5 out of 7 points.

    If this problem doesn't have a score, or we couldn't load it, returns (None,
    None).

    user: a Student object
    block: a BlockStructure's BlockData object
    scores_client: an initialized ScoresClient
    submissions_scores_cache: A dict of location names to (earned, possible) point tuples.
           If an entry is found in this cache, it takes precedence.
    """
    submissions_scores_cache = submissions_scores_cache or {}

    if not user.is_authenticated():
        return (None, None)

    location_url = unicode(block.location)
    if location_url in submissions_scores_cache:
        return submissions_scores_cache[location_url]

    if not getattr(block, 'has_score', False):
        # These are not problems, and do not have a score
        return (None, None)

    # Check the score that comes from the ScoresClient (out of CSM).
    # If an entry exists and has a total associated with it, we trust that
    # value. This is important for cases where a student might have seen an
    # older version of the problem -- they're still graded on what was possible
    # when they tried the problem, not what it's worth now.
    score = scores_client.get(block.location)
    if score and score.total is not None:
        # We have a valid score, just use it.
76 77
        earned = score.correct if score.correct is not None else 0.0
        possible = score.total
78 79 80
    else:
        # This means we don't have a valid score entry and we don't have a
        # cached_max_score on hand. We know they've earned 0.0 points on this.
81 82
        earned = 0.0
        possible = block.transformer_data[GradesTransformer].max_score
83 84

        # Problem may be an error module (if something in the problem builder failed)
85 86
        # In which case possible might be None
        if possible is None:
87 88
            return (None, None)

89
    return weighted_score(earned, possible, block.weight)