module_render.py 19.8 KB
Newer Older
1
import hashlib
2
import json
3
import logging
4
import sys
5

6
from django.conf import settings
7
from django.contrib.auth.models import User
8
from django.core.urlresolvers import reverse
9
from django.http import Http404
10
from django.http import HttpResponse
kimth committed
11
from django.views.decorators.csrf import csrf_exempt
12

13 14
from requests.auth import HTTPBasicAuth

15
from capa.xqueue_interface import XQueueInterface
16
from courseware.access import has_access
17
from mitxmako.shortcuts import render_to_string
18
from models import StudentModule, StudentModuleCache
19
from static_replace import replace_urls
20
from xmodule.errortracker import exc_info_to_str
21
from xmodule.exceptions import NotFoundError
22 23
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
24
from xmodule.x_module import ModuleSystem
25
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
26
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
27

28 29
log = logging.getLogger("mitx.courseware")

30

31
if settings.XQUEUE_INTERFACE['basic_auth'] is not None:
32
    requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
33 34 35
else:
    requests_auth = None

36 37
xqueue_interface = XQueueInterface(
    settings.XQUEUE_INTERFACE['url'],
38
    settings.XQUEUE_INTERFACE['django_auth'],
39
    requests_auth,
40 41 42
)


43
def make_track_function(request):
44
    '''
45
    Make a tracking function that logs what happened.
46
    For use in ModuleSystem.
Piotr Mitros committed
47
    '''
48 49
    import track.views

50 51 52
    def f(event_type, event):
        return track.views.server_track(request, event_type, event, page='x_module')
    return f
53

54

55
def toc_for_course(user, request, course, active_chapter, active_section, course_id=None):
56 57
    '''
    Create a table of contents from the module store
58

59
    Return format:
60 61
    [ {'display_name': name, 'url_name': url_name,
       'sections': SECTIONS, 'active': bool}, ... ]
62

63
    where SECTIONS is a list
64
    [ {'display_name': name, 'url_name': url_name,
kimth committed
65
       'format': format, 'due': due, 'active' : bool, 'graded': bool}, ...]
66

67
    active is set for the section and chapter corresponding to the passed
68 69
    parameters, which are expected to be url_names of the chapter+section.
    Everything else comes from the xml, or defaults to "".
70

71
    chapters with name 'hidden' are skipped.
72 73 74

    NOTE: assumes that if we got this far, user has access to course.  Returns
    None if this is not the case.
75
    '''
76

77 78
    student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
        course_id, user, course, depth=2)
79
    course = get_module(user, request, course.location, student_module_cache, course_id)
80 81
    if course is None:
        return None
82

83 84
    chapters = list()
    for chapter in course.get_display_items():
85 86
        hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true'
        if hide_from_toc:
87
            continue
88

89 90
        sections = list()
        for section in chapter.get_display_items():
Matthew Mongeau committed
91

92 93
            active = (chapter.url_name == active_chapter and
                      section.url_name == active_section)
94
            hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
95

96
            if not hide_from_toc:
97 98
                sections.append({'display_name': section.display_name,
                                 'url_name': section.url_name,
99 100
                                 'format': section.metadata.get('format', ''),
                                 'due': section.metadata.get('due', ''),
101 102 103
                                 'active': active,
                                 'graded': section.metadata.get('graded', False),
                                 })
104

105 106
        chapters.append({'display_name': chapter.display_name,
                         'url_name': chapter.url_name,
107
                         'sections': sections,
108
                         'active': chapter.url_name == active_chapter})
109
    return chapters
110

111

112
def get_section(course_module, chapter, section):
113 114 115
    """
    Returns the xmodule descriptor for the name course > chapter > section,
    or None if this doesn't specify a valid section
116

117
    course: Course url
118 119
    chapter: Chapter url_name
    section: Section url_name
120
    """
121

122 123
    if course_module is None:
        return
124

125 126
    chapter_module = None
    for _chapter in course_module.get_children():
127
        if _chapter.url_name == chapter:
128 129
            chapter_module = _chapter
            break
130

131 132 133 134 135
    if chapter_module is None:
        return

    section_module = None
    for _section in chapter_module.get_children():
136
        if _section.url_name == section:
137 138 139 140
            section_module = _section
            break

    return section_module
141

142
def get_module(user, request, location, student_module_cache, course_id, position=None):
143 144
    """
    Get an instance of the xmodule class identified by location,
145 146
    setting the state based on an existing StudentModule, or creating one if none
    exists.
147 148

    Arguments:
149
      - user                  : User for whom we're getting the module
150 151
      - request               : current django HTTPrequest.  Note: request.user isn't used for anything--all auth
                                and such works based on user.
152
      - location              : A Location-like object identifying the module to load
153
      - student_module_cache  : a StudentModuleCache
154
      - course_id             : the course_id in the context of which to load module
155
      - position              : extra information from URL for user-specified
156
                                position within module
157

158 159 160 161 162 163 164 165 166 167
    Returns: xmodule instance, or None if the user does not have access to the
    module.  If there's an error, will try to return an instance of ErrorModule
    if possible.  If not possible, return None.
    """
    try:
        return _get_module(user, request, location, student_module_cache, course_id, position)
    except:
        # Something has gone terribly wrong, but still not letting it turn into a 500.
        log.exception("Error in get_module")
        return None
168

169 170 171 172 173
def _get_module(user, request, location, student_module_cache, course_id, position=None):
    """
    Actually implement get_module.  See docstring there for details.
    """
    location = Location(location)
174
    descriptor = modulestore().get_instance(course_id, location)
175

176 177 178
    # Short circuit--if the user shouldn't have access, bail without doing any work
    if not has_access(user, descriptor, 'load'):
        return None
179

180
    # Anonymized student identifier
181 182
    h = hashlib.md5()
    h.update(settings.SECRET_KEY)
183 184 185
    h.update(str(user.id))
    anonymous_student_id = h.hexdigest()

186
    # Only check the cache if this module can possibly have state
187 188
    instance_module = None
    shared_module = None
189
    if user.is_authenticated():
190
        if descriptor.stores_state:
191 192
            instance_module = student_module_cache.lookup(
                course_id, descriptor.category, descriptor.location.url())
193

194 195
        shared_state_key = getattr(descriptor, 'shared_state_key', None)
        if shared_state_key is not None:
196 197
            shared_module = student_module_cache.lookup(course_id,
                                                        descriptor.category,
198
                                                        shared_state_key)
199

200 201
    instance_state = instance_module.state if instance_module is not None else None
    shared_state = shared_module.state if shared_module is not None else None
202

203
    # Setup system context for module instance
204
    ajax_url = reverse('modx_dispatch',
205
                       kwargs=dict(course_id=course_id,
206
                                   location=descriptor.location.url(),
207 208
                                   dispatch=''),
                       )
209 210
    # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash.
    ajax_url = ajax_url.rstrip('/')
211

212
    # Fully qualified callback URL for external queueing system
213 214 215 216
    xqueue_callback_url = '{proto}://{host}'.format(
        host=request.get_host(),
        proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
    )
217
    xqueue_callback_url += reverse('xqueue_callback',
218
                                  kwargs=dict(course_id=course_id,
219 220 221 222
                                              userid=str(user.id),
                                              id=descriptor.location.url(),
                                              dispatch='score_update'),
                                  )
223

224
    # Default queuename is course-specific and is derived from the course that
225 226 227
    #   contains the current module.
    # TODO: Queuename should be derived from 'course_settings.json' of each course
    xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
228

229 230
    xqueue = {'interface': xqueue_interface,
              'callback_url': xqueue_callback_url,
kimth committed
231
              'default_queuename': xqueue_default_queuename.replace(' ', '_'),
232
              'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
kimth committed
233
             }
234

235
    def inner_get_module(location):
236 237 238
        """
        Delegate to get_module.  It does an access check, so may return None
        """
239
        return get_module(user, request, location,
240
                                       student_module_cache, course_id, position)
241

242 243 244
    # TODO (cpennington): When modules are shared between courses, the static
    # prefix is going to have to be specific to the module, not the directory
    # that the xml was loaded from
245 246 247
    system = ModuleSystem(track_function=make_track_function(request),
                          render_template=render_to_string,
                          ajax_url=ajax_url,
248
                          xqueue=xqueue,
249 250
                          # TODO (cpennington): Figure out how to share info between systems
                          filestore=descriptor.system.resources_fs,
251
                          get_module=inner_get_module,
252 253 254 255 256
                          user=user,
                          # TODO (cpennington): This should be removed when all html from
                          # a module is coming through get_html and is therefore covered
                          # by the replace_static_urls code below
                          replace_urls=replace_urls,
257
                          node_path=settings.NODE_PATH,
258
                          anonymous_student_id=anonymous_student_id,
259 260
                          )
    # pass position specified in URL to module through ModuleSystem
261
    system.set('position', position)
262
    system.set('DEBUG', settings.DEBUG)
263

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    try:
        module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
    except:
        log.exception("Error creating module from descriptor {0}".format(descriptor))

        # make an ErrorDescriptor -- assuming that the descriptor's system is ok
        import_system = descriptor.system
        if has_access(user, location, 'staff'):
            err_descriptor = ErrorDescriptor.from_xml(str(descriptor), import_system,
                                                      error_msg=exc_info_to_str(sys.exc_info()))
        else:
            err_descriptor = NonStaffErrorDescriptor.from_xml(str(descriptor), import_system,
                                                              error_msg=exc_info_to_str(sys.exc_info()))

        # Make an error module
        return err_descriptor.xmodule_constructor(system)(None, None)
280

281 282
    module.get_html = replace_static_urls(
        wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
283
        module.metadata['data_dir'], module
284
    )
285

286 287 288 289
    # Allow URLs of the form '/course/' refer to the root of multicourse directory
    #   hierarchy of this course
    module.get_html = replace_course_urls(module.get_html, course_id, module)

290
    if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
291
        if has_access(user, module, 'staff'):
292
            module.get_html = add_histogram(module.get_html, module, user)
293

294 295
    return module

296
def get_instance_module(course_id, user, module, student_module_cache):
297 298 299 300
    """
    Returns instance_module is a StudentModule specific to this module for this student,
        or None if this is an anonymous user
    """
301
    if user.is_authenticated():
302
        if not module.descriptor.stores_state:
303
            log.exception("Attempted to get the instance_module for a module "
304 305
                          + str(module.id) + " which does not store state.")
            return None
306

307 308
        instance_module = student_module_cache.lookup(
            course_id, module.category, module.location.url())
309

310 311
        if not instance_module:
            instance_module = StudentModule(
312
                course_id=course_id,
313
                student=user,
314
                module_type=module.category,
315
                module_state_key=module.id,
316 317
                state=module.get_instance_state(),
                max_grade=module.max_score())
318 319 320
            instance_module.save()
            student_module_cache.append(instance_module)

321 322 323
        return instance_module
    else:
        return None
324

325
def get_shared_instance_module(course_id, user, module, student_module_cache):
326 327 328 329 330 331 332
    """
    Return shared_module is a StudentModule specific to all modules with the same
        'shared_state_key' attribute, or None if the module does not elect to
        share state
    """
    if user.is_authenticated():
        # To get the shared_state_key, we need to descriptor
333
        descriptor = modulestore().get_instance(course_id, module.location)
334

335 336 337 338 339 340
        shared_state_key = getattr(module, 'shared_state_key', None)
        if shared_state_key is not None:
            shared_module = student_module_cache.lookup(module.category,
                                                        shared_state_key)
            if not shared_module:
                shared_module = StudentModule(
341
                    course_id=course_id,
342 343 344 345 346 347 348 349
                    student=user,
                    module_type=descriptor.category,
                    module_state_key=shared_state_key,
                    state=module.get_shared_state())
                shared_module.save()
                student_module_cache.append(shared_module)
        else:
            shared_module = None
350

351 352 353
        return shared_module
    else:
        return None
354

355
@csrf_exempt
356
def xqueue_callback(request, course_id, userid, id, dispatch):
357
    '''
358
    Entry point for graded results from the queueing system.
359
    '''
360 361 362
    # Test xqueue package, which we expect to be:
    #   xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}),
    #               'xqueue_body'  : 'Message from grader}
363
    get = request.POST.copy()
364 365
    for key in ['xqueue_header', 'xqueue_body']:
        if not get.has_key(key):
366
            raise Http404
367 368
    header = json.loads(get['xqueue_header'])
    if not isinstance(header, dict) or not header.has_key('lms_key'):
369
        raise Http404
370 371

    # Retrieve target StudentModule
372
    user = User.objects.get(id=userid)
373

374
    student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id,
375
        user, modulestore().get_instance(course_id, id), depth=0, select_for_update=True)
376
    instance = get_module(user, request, id, student_module_cache, course_id)
377
    if instance is None:
378
        log.debug("No module {0} for user {1}--access denied?".format(id, user))
379 380
        raise Http404

381
    instance_module = get_instance_module(course_id, user, instance, student_module_cache)
382

383
    if instance_module is None:
384
        log.debug("Couldn't find instance of module '%s' for user '%s'", id, user)
385
        raise Http404
386

387 388 389
    oldgrade = instance_module.grade
    old_instance_state = instance_module.state

390 391
    # Transfer 'queuekey' from xqueue response header to 'get'. This is required to
    #   use the interface defined by 'handle_ajax'
392
    get.update({'queuekey': header['lms_key']})
393

394 395 396
    # We go through the "AJAX" path
    #   So far, the only dispatch from xqueue will be 'score_update'
    try:
397
        ajax_return = instance.handle_ajax(dispatch, get)  # Can ignore the "ajax" return in 'xqueue_callback'
398 399 400 401 402 403 404 405 406 407 408 409 410
    except:
        log.exception("error processing ajax call")
        raise

    # Save state back to database
    instance_module.state = instance.get_instance_state()
    if instance.get_score():
        instance_module.grade = instance.get_score()['score']
    if instance_module.grade != oldgrade or instance_module.state != old_instance_state:
        instance_module.save()

    return HttpResponse("")

411

412
def modx_dispatch(request, dispatch, location, course_id):
413 414 415 416 417 418 419 420
    ''' Generic view for extensions. This is where AJAX calls go.

    Arguments:

      - request -- the django request.
      - dispatch -- the command string to pass through to the module's handle_ajax call
           (e.g. 'problem_reset').  If this string contains '?', only pass
           through the part before the first '?'.
421 422
      - location -- the module location. Used to look up the XModule instance
      - course_id -- defines the course context for this request.
423
    '''
424
    # ''' (fix emacs broken parsing)
425

426 427 428 429
    # Check parameters and fail fast if there's a problem
    if not Location.is_valid(location):
        raise Http404("Invalid location")

430
    # Check for submitted files and basic file size checks
431
    p = request.POST.copy()
432
    if request.FILES:
kimth committed
433 434
        for fileinput_id in request.FILES.keys():
            inputfiles = request.FILES.getlist(fileinput_id)
435 436 437 438 439 440

            if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT:
                too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' %\
                    settings.MAX_FILEUPLOADS_PER_INPUT
                return HttpResponse(json.dumps({'success': too_many_files_msg}))

kimth committed
441 442 443 444 445 446
            for inputfile in inputfiles:
                if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
                    file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
                                        (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE/(1000**2))
                    return HttpResponse(json.dumps({'success': file_too_big_msg}))
            p[fileinput_id] = inputfiles
447

448
    student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id,
449
        request.user, modulestore().get_instance(course_id, location))
450 451

    instance = get_module(request.user, request, location, student_module_cache, course_id)
452 453 454
    if instance is None:
        # Either permissions just changed, or someone is trying to be clever
        # and load something they shouldn't have access to.
455
        log.debug("No module {0} for user {1}--access denied?".format(location, user))
456
        raise Http404
457

458
    instance_module = get_instance_module(course_id, request.user, instance, student_module_cache)
459
    shared_module = get_shared_instance_module(course_id, request.user, instance, student_module_cache)
460

461 462 463 464 465
    # Don't track state for anonymous users (who don't have student modules)
    if instance_module is not None:
        oldgrade = instance_module.grade
        old_instance_state = instance_module.state
        old_shared_state = shared_module.state if shared_module is not None else None
466 467

    # Let the module handle the AJAX
468
    try:
469
        ajax_return = instance.handle_ajax(dispatch, p)
470 471 472
    except NotFoundError:
        log.exception("Module indicating to user that request doesn't exist")
        raise Http404
473 474 475
    except:
        log.exception("error processing ajax call")
        raise
476

477
    # Save the state back to the database
478 479 480 481 482 483 484
    # Don't track state for anonymous users (who don't have student modules)
    if instance_module is not None:
        instance_module.state = instance.get_instance_state()
        if instance.get_score():
            instance_module.grade = instance.get_score()['score']
        if instance_module.grade != oldgrade or instance_module.state != old_instance_state:
            instance_module.save()
485 486 487 488 489

    if shared_module is not None:
        shared_module.state = instance.get_shared_state()
        if shared_module.state != old_shared_state:
            shared_module.save()
490

491 492
    # Return whatever the module wanted to return to the client/caller
    return HttpResponse(ajax_return)