runtime.py 6.58 KB
Newer Older
1 2 3 4 5
"""
Module implementing `xblock.runtime.Runtime` functionality for the LMS
"""

import re
swdanielli committed
6
import xblock.reference.plugins
7 8

from django.core.urlresolvers import reverse
9
from django.conf import settings
10
from user_api.api import course_tag as user_course_tag_api
11
from xmodule.modulestore.django import modulestore
12
from xmodule.x_module import ModuleSystem
13
from xmodule.partitions.partitions_service import PartitionService
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


def _quote_slashes(match):
    """
    Helper function for `quote_slashes`
    """
    matched = match.group(0)
    # We have to escape ';', because that is our
    # escape sequence identifier (otherwise, the escaping)
    # couldn't distinguish between us adding ';_' to the string
    # and ';_' appearing naturally in the string
    if matched == ';':
        return ';;'
    elif matched == '/':
        return ';_'
    else:
        return matched


def quote_slashes(text):
    """
    Quote '/' characters so that they aren't visible to
    django's url quoting, unquoting, or url regex matching.

    Escapes '/'' to the sequence ';_', and ';' to the sequence
    ';;'. By making the escape sequence fixed length, and escaping
    identifier character ';', we are able to reverse the escaping.
    """
42
    return re.sub(ur'[;/]', _quote_slashes, text)
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


def _unquote_slashes(match):
    """
    Helper function for `unquote_slashes`
    """
    matched = match.group(0)
    if matched == ';;':
        return ';'
    elif matched == ';_':
        return '/'
    else:
        return matched


def unquote_slashes(text):
    """
    Unquote slashes quoted by `quote_slashes`
    """
    return re.sub(r'(;;|;_)', _unquote_slashes, text)


class LmsHandlerUrls(object):
    """
    A runtime mixin that provides a handler_url function that routes
    to the LMS' xblock handler view.

    This must be mixed in to a runtime that already accepts and stores
    a course_id
    """
73 74 75
    # pylint: disable=unused-argument
    # pylint: disable=no-member
    def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
76
        """See :method:`xblock.runtime:Runtime.handler_url`"""
77 78 79 80 81 82 83 84 85 86 87 88 89
        view_name = 'xblock_handler'
        if handler_name:
            # Be sure this is really a handler.
            func = getattr(block, handler_name, None)
            if not func:
                raise ValueError("{!r} is not a function name".format(handler_name))
            if not getattr(func, "_is_xblock_handler", False):
                raise ValueError("{!r} is not a handler name".format(handler_name))

        if thirdparty:
            view_name = 'xblock_handler_noauth'

        url = reverse(view_name, kwargs={
90 91
            'course_id': self.course_id.to_deprecated_string(),
            'usage_id': quote_slashes(block.scope_ids.usage_id.to_deprecated_string().encode('utf-8')),
92 93 94 95 96 97 98 99 100 101 102 103
            'handler': handler_name,
            'suffix': suffix,
        })

        # If suffix is an empty string, remove the trailing '/'
        if not suffix:
            url = url.rstrip('/')

        # If there is a query string, append it
        if query:
            url += '?' + query

104 105 106 107 108 109 110 111 112
        # If third-party, return fully-qualified url
        if thirdparty:
            scheme = "https" if settings.HTTPS == "on" else "http"
            url = '{scheme}://{host}{path}'.format(
                scheme=scheme,
                host=settings.SITE_NAME,
                path=url
            )

113
        return url
114

115 116 117 118 119 120 121 122 123
    def local_resource_url(self, block, uri):
        """
        local_resource_url for Studio
        """
        return reverse('xblock_resource_url', kwargs={
            'block_type': block.scope_ids.block_type,
            'uri': uri,
        })

124

125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
class LmsPartitionService(PartitionService):
    """
    Another runtime mixin that provides access to the student partitions defined on the
    course.

    (If and when XBlock directly provides access from one block (e.g. a split_test_module)
    to another (e.g. a course_module), this won't be neccessary, but for now it seems like
    the least messy way to hook things through)

    """
    @property
    def course_partitions(self):
        course = modulestore().get_course(self._course_id)
        return course.user_partitions


class UserTagsService(object):
    """
    A runtime class that provides an interface to the user service.  It handles filling in
    the current course id and current user.
    """
146

147
    COURSE_SCOPE = user_course_tag_api.COURSE_SCOPE
148 149 150 151 152

    def __init__(self, runtime):
        self.runtime = runtime

    def _get_current_user(self):
153
        """Returns the real, not anonymized, current user."""
154 155 156 157 158 159 160 161 162 163
        real_user = self.runtime.get_real_user(self.runtime.anonymous_student_id)
        return real_user

    def get_tag(self, scope, key):
        """
        Get a user tag for the current course and the current user for a given key

            scope: the current scope of the runtime
            key: the key for the value we want
        """
164
        if scope != user_course_tag_api.COURSE_SCOPE:
165 166
            raise ValueError("unexpected scope {0}".format(scope))

167 168 169 170
        return user_course_tag_api.get_course_tag(
            self._get_current_user(),
            self.runtime.course_id, key
        )
171 172 173 174 175 176 177 178 179

    def set_tag(self, scope, key, value):
        """
        Set the user tag for the current course and the current user for a given key

            scope: the current scope of the runtime
            key: the key that to the value to be set
            value: the value to set
        """
180
        if scope != user_course_tag_api.COURSE_SCOPE:
181 182
            raise ValueError("unexpected scope {0}".format(scope))

183 184 185 186
        return user_course_tag_api.set_course_tag(
            self._get_current_user(),
            self.runtime.course_id, key, value
        )
187 188


189 190 191 192
class LmsModuleSystem(LmsHandlerUrls, ModuleSystem):  # pylint: disable=abstract-method
    """
    ModuleSystem specialized to the LMS
    """
193 194 195 196 197 198 199 200
    def __init__(self, **kwargs):
        services = kwargs.setdefault('services', {})
        services['user_tags'] = UserTagsService(self)
        services['partitions'] = LmsPartitionService(
            user_tags_service=services['user_tags'],
            course_id=kwargs.get('course_id', None),
            track_function=kwargs.get('track_function', None),
        )
swdanielli committed
201
        services['fs'] = xblock.reference.plugins.FSService()
202
        super(LmsModuleSystem, self).__init__(**kwargs)
203 204 205 206 207 208 209 210

    # backward compatibility fix for callers not knowing this is a ModuleSystem v DescriptorSystem
    @property
    def resources_fs(self):
        """
        Return what would be the resources_fs on a DescriptorSystem
        """
        return getattr(self, 'filestore', None)