library_content.py 6.67 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
"""
Content Library Transformer.
"""
import json
from courseware.models import StudentModule
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
from xmodule.library_content_module import LibraryContentModule
from xmodule.modulestore.django import modulestore
from eventtracking import tracker


class ContentLibraryTransformer(BlockStructureTransformer):
    """
    A transformer that manipulates the block structure by removing all
    blocks within a library_content module to which a user should not
    have access.

    Staff users are *not* exempted from library content pathways.
    """
    VERSION = 1

    @classmethod
    def name(cls):
        """
        Unique identifier for the transformer's class;
        same identifier used in setup.py.
        """
        return "library_content"

    @classmethod
    def collect(cls, block_structure):
        """
        Collects any information that's necessary to execute this
        transformer's transform method.
        """
        block_structure.request_xblock_fields('mode')
        block_structure.request_xblock_fields('max_count')
        block_structure.request_xblock_fields('category')
        store = modulestore()

        # needed for analytics purposes
        def summarize_block(usage_key):
            """ Basic information about the given block """
            orig_key, orig_version = store.get_block_original_usage(usage_key)
            return {
                "usage_key": unicode(usage_key),
                "original_usage_key": unicode(orig_key) if orig_key else None,
                "original_usage_version": unicode(orig_version) if orig_version else None,
            }

        # For each block check if block is library_content.
        # If library_content add children array to content_library_children field
        for block_key in block_structure.topological_traversal(
                filter_func=lambda block_key: block_key.block_type == 'library_content',
                yield_descendants_of_unyielded=True,
        ):
            xblock = block_structure.get_xblock(block_key)
            for child_key in xblock.children:
                summary = summarize_block(child_key)
                block_structure.set_transformer_block_field(child_key, cls, 'block_analytics_summary', summary)

    def transform(self, usage_info, block_structure):
        """
        Mutates block_structure based on the given usage_info.
        """

        all_library_children = set()
        all_selected_children = set()
        for block_key in block_structure.topological_traversal(
                filter_func=lambda block_key: block_key.block_type == 'library_content',
                yield_descendants_of_unyielded=True,
        ):
            library_children = block_structure.get_children(block_key)
            if library_children:
                all_library_children.update(library_children)
                selected = []
                mode = block_structure.get_xblock_field(block_key, 'mode')
                max_count = block_structure.get_xblock_field(block_key, 'max_count')

                # Retrieve "selected" json from LMS MySQL database.
                module = self._get_student_module(usage_info.user, usage_info.course_key, block_key)
                if module:
                    state_dict = json.loads(module.state)
                    # Add all selected entries for this user for this
                    # library module to the selected list.
                    for state in state_dict['selected']:
                        usage_key = usage_info.course_key.make_usage_key(state[0], state[1])
                        if usage_key in library_children:
                            selected.append((state[0], state[1]))

                # update selected
                previous_count = len(selected)
                block_keys = LibraryContentModule.make_selection(selected, library_children, max_count, mode)
                selected = block_keys['selected']

                # publish events for analytics
                self._publish_events(block_structure, block_key, previous_count, max_count, block_keys)
                all_selected_children.update(usage_info.course_key.make_usage_key(s[0], s[1]) for s in selected)

        def check_child_removal(block_key):
            """
            Return True if selected block should be removed.

            Block is removed if it is part of library_content, but has
            not been selected for current user.
            """
            if block_key not in all_library_children:
                return False
            if block_key in all_selected_children:
                return False
            return True

        # Check and remove all non-selected children from course
        # structure.
        block_structure.remove_block_if(
            check_child_removal
        )

    @classmethod
    def _get_student_module(cls, user, course_key, block_key):
        """
        Get the student module for the given user for the given block.

        Arguments:
            user (User)
            course_key (CourseLocator)
            block_key (BlockUsageLocator)

        Returns:
            StudentModule if exists, or None.
        """
        try:
            return StudentModule.objects.get(
                student=user,
                course_id=course_key,
                module_state_key=block_key,
                state__contains='"selected": [['
            )
        except StudentModule.DoesNotExist:
            return None

    @classmethod
    def _publish_events(cls, block_structure, location, previous_count, max_count, block_keys):
        """
        Helper method to publish events for analytics purposes
        """

        def format_block_keys(keys):
            """
            Helper function to format block keys
            """
            json_result = []
            for key in keys:
                info = block_structure.get_transformer_block_field(
                    key, ContentLibraryTransformer, 'block_analytics_summary'
                )
                json_result.append(info)
            return json_result

        def publish_event(event_name, result, **kwargs):
            """
            Helper function to publish an event for analytics purposes
            """
            event_data = {
                "location": unicode(location),
                "previous_count": previous_count,
                "result": result,
                "max_count": max_count
            }
            event_data.update(kwargs)
            tracker.emit("edx.librarycontentblock.content.{}".format(event_name), event_data)

        LibraryContentModule.publish_selected_children_events(
            block_keys,
            format_block_keys,
            publish_event,
        )