subsection_grade.py 9.65 KB
Newer Older
1 2 3
"""
SubsectionGrade Class
"""
4
from abc import ABCMeta
5
from collections import OrderedDict
6
from logging import getLogger
7 8 9

from lazy import lazy

10
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
11
from lms.djangoapps.grades.scores import get_score, possibly_scored
12
from xmodule import block_metadata_utils, graders
13
from xmodule.graders import AggregatedScore, ShowCorrectness
14 15


16 17 18
log = getLogger(__name__)


19
class SubsectionGradeBase(object):
20
    """
21
    Abstract base class for Subsection Grades.
22
    """
23 24
    __metaclass__ = ABCMeta

25
    def __init__(self, subsection):
26 27 28 29 30 31 32
        self.location = subsection.location
        self.display_name = block_metadata_utils.display_name_with_default_escaped(subsection)
        self.url_name = block_metadata_utils.url_name_for_block(subsection)

        self.format = getattr(subsection, 'format', '')
        self.due = getattr(subsection, 'due', None)
        self.graded = getattr(subsection, 'graded', False)
33
        self.show_correctness = getattr(subsection, 'show_correctness', '')
34

35
        self.course_version = getattr(subsection, 'course_version', None)
36
        self.subtree_edited_timestamp = getattr(subsection, 'subtree_edited_on', None)
37

Tyler Hallada committed
38 39
        self.override = None

40
    @property
41
    def attempted(self):
42
        """
43 44
        Returns whether any problem in this subsection
        was attempted by the student.
45
        """
46 47 48 49 50 51

        assert self.all_total is not None, (
            "SubsectionGrade not fully populated yet.  Call init_from_structure or init_from_model "
            "before use."
        )
        return self.all_total.attempted
52

53 54 55 56 57 58
    def show_grades(self, has_staff_access):
        """
        Returns whether subsection scores are currently available to users with or without staff access.
        """
        return ShowCorrectness.correctness_available(self.show_correctness, self.due, has_staff_access)

59 60 61 62 63 64 65
    @property
    def attempted_graded(self):
        """
        Returns whether the user had attempted a graded problem in this subsection.
        """
        raise NotImplementedError

66 67 68 69 70 71 72
    @property
    def percent_graded(self):
        """
        Returns the percent score of the graded problems in this subsection.
        """
        raise NotImplementedError

73 74 75 76 77

class ZeroSubsectionGrade(SubsectionGradeBase):
    """
    Class for Subsection Grades with Zero values.
    """
78

79 80 81 82
    def __init__(self, subsection, course_data):
        super(ZeroSubsectionGrade, self).__init__(subsection)
        self.course_data = course_data

83
    @property
84 85 86 87
    def attempted_graded(self):
        return False

    @property
88 89 90 91
    def percent_graded(self):
        return 0.0

    @property
92 93 94 95 96 97 98 99 100 101 102
    def all_total(self):
        return self._aggregate_scores[0]

    @property
    def graded_total(self):
        return self._aggregate_scores[1]

    @lazy
    def _aggregate_scores(self):
        return graders.aggregate_scores(self.problem_scores.values())

103
    @lazy
104
    def problem_scores(self):
105
        """
106
        Overrides the problem_scores member variable in order
107 108 109 110 111 112 113 114 115 116
        to return empty scores for all scorable problems in the
        course.
        """
        locations = OrderedDict()  # dict of problem locations to ProblemScore
        for block_key in self.course_data.structure.post_order_traversal(
                filter_func=possibly_scored,
                start_node=self.location,
        ):
            block = self.course_data.structure[block_key]
            if getattr(block, 'has_score', False):
117
                problem_score = get_score(
118 119
                    submissions_scores={}, csm_scores={}, persisted_block=None, block=block,
                )
120 121
                if problem_score is not None:
                    locations[block_key] = problem_score
122 123 124
        return locations


125
class NonZeroSubsectionGrade(SubsectionGradeBase):
126
    """
127 128
    Abstract base class for Subsection Grades with
    possibly NonZero values.
129
    """
130 131 132 133
    __metaclass__ = ABCMeta

    def __init__(self, subsection, all_total, graded_total, override=None):
        super(NonZeroSubsectionGrade, self).__init__(subsection)
Nimisha Asthagiri committed
134 135 136
        self.all_total = all_total
        self.graded_total = graded_total
        self.override = override
137

138 139 140
    @property
    def attempted_graded(self):
        return self.graded_total.first_attempted is not None
141

142 143
    @property
    def percent_graded(self):
144 145 146 147
        if self.graded_total.possible > 0:
            return self.graded_total.earned / self.graded_total.possible
        else:
            return 0.0
148

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    @staticmethod
    def _compute_block_score(
            block_key,
            course_structure,
            submissions_scores,
            csm_scores,
            persisted_block=None,
    ):
        try:
            block = course_structure[block_key]
        except KeyError:
            # It's possible that the user's access to that
            # block has changed since the subsection grade
            # was last persisted.
            pass
        else:
            if getattr(block, 'has_score', False):
                return get_score(
                    submissions_scores,
                    csm_scores,
                    persisted_block,
                    block,
                )
172

173

174 175 176 177
class ReadSubsectionGrade(NonZeroSubsectionGrade):
    """
    Class for Subsection grades that are read from the database.
    """
178
    def __init__(self, subsection, model, factory):
Nimisha Asthagiri committed
179
        all_total = AggregatedScore(
180 181
            tw_earned=model.earned_all,
            tw_possible=model.possible_all,
182
            graded=False,
183
            first_attempted=model.first_attempted,
184
        )
Nimisha Asthagiri committed
185 186 187 188 189 190 191
        graded_total = AggregatedScore(
            tw_earned=model.earned_graded,
            tw_possible=model.possible_graded,
            graded=True,
            first_attempted=model.first_attempted,
        )
        override = model.override if hasattr(model, 'override') else None
192 193 194

        # save these for later since we compute problem_scores lazily
        self.model = model
195
        self.factory = factory
196 197 198 199 200 201 202 203

        super(ReadSubsectionGrade, self).__init__(subsection, all_total, graded_total, override)

    @lazy
    def problem_scores(self):
        problem_scores = OrderedDict()
        for block in self.model.visible_blocks.blocks:
            problem_score = self._compute_block_score(
204 205 206 207 208
                block.locator,
                self.factory.course_data.structure,
                self.factory._submissions_scores,
                self.factory._csm_scores,
                block,
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
            )
            if problem_score:
                problem_scores[block.locator] = problem_score
        return problem_scores


class CreateSubsectionGrade(NonZeroSubsectionGrade):
    """
    Class for Subsection grades that are newly created or updated.
    """
    def __init__(self, subsection, course_structure, submissions_scores, csm_scores):
        self.problem_scores = OrderedDict()
        for block_key in course_structure.post_order_traversal(
                filter_func=possibly_scored,
                start_node=subsection.location,
        ):
            problem_score = self._compute_block_score(block_key, course_structure, submissions_scores, csm_scores)
            if problem_score:
                self.problem_scores[block_key] = problem_score

        all_total, graded_total = graders.aggregate_scores(self.problem_scores.values())

        super(CreateSubsectionGrade, self).__init__(subsection, all_total, graded_total)

    def update_or_create_model(self, student, score_deleted=False):
        """
        Saves or updates the subsection grade in a persisted model.
        """
        if self._should_persist_per_attempted(score_deleted):
            return PersistentSubsectionGrade.update_or_create_grade(**self._persisted_model_params(student))
239

240 241
    @classmethod
    def bulk_create_models(cls, student, subsection_grades, course_key):
242
        """
243
        Saves the subsection grade in a persisted model.
244
        """
245
        params = [
246 247 248
            subsection_grade._persisted_model_params(student)  # pylint: disable=protected-access
            for subsection_grade in subsection_grades
            if subsection_grade
249 250
            if subsection_grade._should_persist_per_attempted()  # pylint: disable=protected-access
        ]
Nimisha Asthagiri committed
251
        return PersistentSubsectionGrade.bulk_create_grades(params, student.id, course_key)
252

253
    def _should_persist_per_attempted(self, score_deleted=False):
254 255 256
        """
        Returns whether the SubsectionGrade's model should be
        persisted based on settings and attempted status.
257 258 259

        If the learner's score was just deleted, they will have
        no attempts but the grade should still be persisted.
260
        """
261 262 263 264
        return (
            self.all_total.first_attempted is not None or
            score_deleted
        )
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280

    def _persisted_model_params(self, student):
        """
        Returns the parameters for creating/updating the
        persisted model for this subsection grade.
        """
        return dict(
            user_id=student.id,
            usage_key=self.location,
            course_version=self.course_version,
            subtree_edited_timestamp=self.subtree_edited_timestamp,
            earned_all=self.all_total.earned,
            possible_all=self.all_total.possible,
            earned_graded=self.graded_total.earned,
            possible_graded=self.graded_total.possible,
            visible_blocks=self._get_visible_blocks,
281
            first_attempted=self.all_total.first_attempted,
282 283 284 285 286 287 288 289
        )

    @property
    def _get_visible_blocks(self):
        """
        Returns the list of visible blocks.
        """
        return [
290 291
            BlockRecord(location, score.weight, score.raw_possible, score.graded)
            for location, score in
292
            self.problem_scores.iteritems()
293
        ]