import logging import urllib from functools import partial from django.conf import settings from django.core.context_processors import csrf from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.http import Http404, HttpResponseRedirect from django.shortcuts import redirect from mitxmako.shortcuts import render_to_response, render_to_string #from django.views.decorators.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university, sort_by_announcement) import courseware.tabs as tabs from courseware.models import StudentModule, StudentModuleCache from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor from django_comment_client.utils import get_discussion_title from student.models import UserTestGroup, CourseEnrollment from util.cache import cache, cache_if_anonymous from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.search import path_to_location import comment_client log = logging.getLogger("mitx.courseware") template_imports = {'urllib': urllib} def user_groups(user): """ TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately. """ if not user.is_authenticated(): return [] # TODO: Rewrite in Django key = 'user_group_names_{user.id}'.format(user=user) cache_expiration = 60 * 60 # one hour # Kill caching on dev machines -- we switch groups a lot group_names = cache.get(key) if settings.DEBUG: group_names = None if group_names is None: group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] cache.set(key, group_names, cache_expiration) return group_names @ensure_csrf_cookie @cache_if_anonymous def courses(request): ''' Render "find courses" page. The course selection work is done in courseware.courses. ''' courses = get_courses(request.user, request.META.get('HTTP_HOST')) courses = sort_by_announcement(courses) return render_to_response("courseware/courses.html", {'courses': courses}) def render_accordion(request, course, chapter, section): ''' Draws navigation bar. Takes current position in accordion as parameter. If chapter and section are '' or None, renders a default accordion. course, chapter, and section are the url_names. Returns the html string''' # grab the table of contents toc = toc_for_course(request.user, request, course, chapter, section) context = dict([('toc', toc), ('course_id', course.id), ('csrf', csrf(request)['csrf_token'])] + template_imports.items()) return render_to_string('courseware/accordion.html', context) def get_current_child(xmodule): """ Get the xmodule.position's display item of an xmodule that has a position and children. Returns None if the xmodule doesn't have a position, or if there are no children. Otherwise, if position is out of bounds, returns the first child. """ if not hasattr(xmodule, 'position'): return None children = xmodule.get_display_items() # position is 1-indexed. if 0 <= xmodule.position - 1 < len(children): child = children[xmodule.position - 1] elif len(children) > 0: # Something is wrong. Default to first child child = children[0] else: child = None return child def redirect_to_course_position(course_module, first_time): """ Load the course state for the user, and return a redirect to the appropriate place in the course: either the first element if there is no state, or their previous place if there is. If this is the user's first time, send them to the first section instead. """ course_id = course_module.descriptor.id chapter = get_current_child(course_module) if chapter is None: # oops. Something bad has happened. raise Http404 if not first_time: return redirect(reverse('courseware_chapter', kwargs={'course_id': course_id, 'chapter': chapter.url_name})) # Relying on default of returning first child section = get_current_child(chapter) return redirect(reverse('courseware_section', kwargs={'course_id': course_id, 'chapter': chapter.url_name, 'section': section.url_name})) def save_child_position(seq_module, child_name, instance_module): """ child_name: url_name of the child instance_module: the StudentModule object for the seq_module """ for i, c in enumerate(seq_module.get_display_items()): if c.url_name == child_name: # Position is 1-indexed position = i + 1 # Only save if position changed if position != seq_module.position: seq_module.position = position instance_module.state = seq_module.get_instance_state() instance_module.save() def check_for_active_timelimit_module(request, course_id, course): ''' Looks for a timing module for the given user and course that is currently active. If found, returns a context dict with timer-related values to enable display of time remaining. ''' context = {} timelimit_student_modules = StudentModule.objects.filter(student=request.user, course_id=course_id, module_type='timelimit') if timelimit_student_modules: for timelimit_student_module in timelimit_student_modules: # get the corresponding section_descriptor for the given StudentModel entry: module_state_key = timelimit_student_module.module_state_key timelimit_descriptor = modulestore().get_instance(course_id, Location(module_state_key)) timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course.id, request.user, timelimit_descriptor, depth=None) timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, timelimit_module_cache, course.id, position=None) if timelimit_module is not None and timelimit_module.category == 'timelimit' and \ timelimit_module.has_begun and not timelimit_module.has_ended: location = timelimit_module.location # determine where to go when the timer expires: if 'time_expired_redirect_url' not in timelimit_descriptor.metadata: raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location)) time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url') context['time_expired_redirect_url'] = time_expired_redirect_url # Fetch the remaining time relative to the end time as stored in the module when it was started. # This value should be in milliseconds. remaining_time = timelimit_module.get_remaining_time_in_ms() context['timer_expiration_duration'] = remaining_time if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata: context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation'] return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location}) context['timer_navigation_return_url'] = return_url return context def update_timelimit_module(user, course_id, student_module_cache, timelimit_descriptor, timelimit_module): ''' Updates the state of the provided timing module, starting it if it hasn't begun. Returns dict with timer-related values to enable display of time remaining. Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired. ''' context = {} # determine where to go when the exam ends: if 'time_expired_redirect_url' not in timelimit_descriptor.metadata: raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', timelimit_module.location)) time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url') context['time_expired_redirect_url'] = time_expired_redirect_url if not timelimit_module.has_ended: if not timelimit_module.has_begun: # user has not started the exam, so start it now. if 'duration' not in timelimit_descriptor.metadata: raise Http404("No {0} metadata at this location: {1} ".format('duration', timelimit_module.location)) # The user may have an accommodation that has been granted to them. # This accommodation information should already be stored in the module's state. duration = int(timelimit_descriptor.metadata.get('duration')) timelimit_module.begin(duration) # we have changed state, so we need to persist the change: instance_module = get_instance_module(course_id, user, timelimit_module, student_module_cache) instance_module.state = timelimit_module.get_instance_state() instance_module.save() # the exam has been started, either because the student is returning to the # exam page, or because they have just visited it. Fetch the remaining time relative to the # end time as stored in the module when it was started. context['timer_expiration_duration'] = timelimit_module.get_remaining_time_in_ms() # also use the timed module to determine whether top-level navigation is visible: if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata: context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation'] return context @login_required @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def index(request, course_id, chapter=None, section=None, position=None): """ Displays courseware accordion and associated content. If course, chapter, and section are all specified, renders the page, or returns an error if they are invalid. If section is not specified, displays the accordion opened to the right chapter. If neither chapter or section are specified, redirects to user's most recent chapter, or the first chapter if this is the user's first visit. Arguments: - request : HTTP request - course_id : course id (str: ORG/course/URL_NAME) - chapter : chapter url_name (str) - section : section url_name (str) - position : position in module, eg of module (str) Returns: - HTTPresponse """ course = get_course_with_access(request.user, course_id, 'load', depth=2) staff_access = has_access(request.user, course, 'staff') registered = registered_for_course(course, request.user) if not registered: # TODO (vshnayder): do course instructors need to be registered to see course? log.debug('User %s tried to view course %s but is not enrolled' % (request.user, course.location.url())) return redirect(reverse('about_course', args=[course.id])) try: student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( course.id, request.user, course, depth=2) # Has this student been in this course before? first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None # Load the module for the course course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id) if course_module is None: log.warning('If you see this, something went wrong: if we got this' ' far, should have gotten a course module for this user') return redirect(reverse('about_course', args=[course.id])) if chapter is None: return redirect_to_course_position(course_module, first_time) context = { 'csrf': csrf(request)['csrf_token'], 'accordion': render_accordion(request, course, chapter, section), 'COURSE_TITLE': course.title, 'course': course, 'init': '', 'content': '', 'staff_access': staff_access, 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa') } chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter) if chapter_descriptor is not None: instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache) save_child_position(course_module, chapter, instance_module) else: raise Http404('No chapter descriptor found with name {}'.format(chapter)) chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter) if chapter_module is None: # User may be trying to access a chapter that isn't live yet raise Http404 if section is not None: section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section) if section_descriptor is None: # Specifically asked-for section doesn't exist raise Http404 # Load all descendants of the section, because we're going to display its # html, which in general will need all of its children section_module_cache = StudentModuleCache.cache_for_descriptor_descendents( course.id, request.user, section_descriptor, depth=None) section_module = get_module(request.user, request, section_descriptor.location, section_module_cache, course.id, position=position, depth=None) if section_module is None: # User may be trying to be clever and access something # they don't have access to. raise Http404 # Save where we are in the chapter instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache) save_child_position(chapter_module, section, instance_module) # check here if this section *is* a timed module. if section_module.category == 'timelimit': timer_context = update_timelimit_module(request.user, course_id, student_module_cache, section_descriptor, section_module) if 'timer_expiration_duration' in timer_context: context.update(timer_context) else: # if there is no expiration defined, then we know the timer has expired: return HttpResponseRedirect(timer_context['time_expired_redirect_url']) else: # check here if this page is within a course that has an active timed module running. If so, then # add in the appropriate timer information to the rendering context: context.update(check_for_active_timelimit_module(request, course_id, course)) context['content'] = section_module.get_html() else: # section is none, so display a message prev_section = get_current_child(chapter_module) if prev_section is None: # Something went wrong -- perhaps this chapter has no sections visible to the user raise Http404 prev_section_url = reverse('courseware_section', kwargs={'course_id': course_id, 'chapter': chapter_descriptor.url_name, 'section': prev_section.url_name}) context['content'] = render_to_string('courseware/welcome-back.html', {'course': course, 'chapter_module': chapter_module, 'prev_section': prev_section, 'prev_section_url': prev_section_url}) result = render_to_response('courseware/courseware.html', context) except Exception as e: if isinstance(e, Http404): # let it propagate raise # In production, don't want to let a 500 out for any reason if settings.DEBUG: raise else: log.exception("Error in index view: user={user}, course={course}," " chapter={chapter} section={section}" "position={position}".format( user=request.user, course=course, chapter=chapter, section=section, position=position )) try: result = render_to_response('courseware/courseware-error.html', {'staff_access': staff_access, 'course': course}) except: # Let the exception propagate, relying on global config to at # at least return a nice error message log.exception("Error while rendering courseware-error page") raise return result @ensure_csrf_cookie def jump_to(request, course_id, location): ''' Show the page that contains a specific location. If the location is invalid or not in any class, return a 404. Otherwise, delegates to the index view to figure out whether this user has access, and what they should see. ''' # Complain if the location isn't valid try: location = Location(location) except InvalidLocationError: raise Http404("Invalid location") # Complain if there's not data for this location try: (course_id, chapter, section, position) = path_to_location(modulestore(), course_id, location) except ItemNotFoundError: raise Http404("No data at this location: {0}".format(location)) except NoPathToItem: raise Http404("This location is not in any class: {0}".format(location)) # choose the appropriate view (and provide the necessary args) based on the # args provided by the redirect. # Rely on index to do all error handling and access control. if chapter is None: return redirect('courseware', course_id=course_id) elif section is None: return redirect('courseware_chapter', course_id=course_id, chapter=chapter) elif position is None: return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section) else: return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position) @ensure_csrf_cookie def course_info(request, course_id): """ Display the course's info.html, or 404 if there is no such course. Assumes the course_id is in a valid format. """ course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') return render_to_response('courseware/info.html', {'request': request, 'course_id': course_id, 'cache': None, 'course': course, 'staff_access': staff_access}) @ensure_csrf_cookie def static_tab(request, course_id, tab_slug): """ Display the courses tab with the given name. Assumes the course_id is in a valid format. """ course = get_course_with_access(request.user, course_id, 'load') tab = tabs.get_static_tab_by_slug(course, tab_slug) if tab is None: raise Http404 contents = tabs.get_static_tab_contents(request, None, course, tab) if contents is None: raise Http404 staff_access = has_access(request.user, course, 'staff') return render_to_response('courseware/static_tab.html', {'course': course, 'tab': tab, 'tab_contents': contents, 'staff_access': staff_access, }) # TODO arjun: remove when custom tabs in place, see courseware/syllabus.py @ensure_csrf_cookie def syllabus(request, course_id): """ Display the course's syllabus.html, or 404 if there is no such course. Assumes the course_id is in a valid format. """ course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') return render_to_response('courseware/syllabus.html', {'course': course, 'staff_access': staff_access, }) def registered_for_course(course, user): '''Return CourseEnrollment if user is registered for course, else False''' if user is None: return False if user.is_authenticated(): return CourseEnrollment.objects.filter(user=user, course_id=course.id).exists() else: return False @ensure_csrf_cookie @cache_if_anonymous def course_about(request, course_id): course = get_course_with_access(request.user, course_id, 'see_exists') registered = registered_for_course(course, request.user) if has_access(request.user, course, 'load'): course_target = reverse('info', args=[course.id]) else: course_target = reverse('about_course', args=[course.id]) show_courseware_link = (has_access(request.user, course, 'load') or settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION')) return render_to_response('courseware/course_about.html', {'course': course, 'registered': registered, 'course_target': course_target, 'show_courseware_link': show_courseware_link}) @ensure_csrf_cookie @cache_if_anonymous def static_university_profile(request, org_id): """ Return the profile for the particular org_id that does not have any courses. """ template_file = "university_profile/{0}.html".format(org_id).lower() context = dict(courses=[], org_id=org_id) return render_to_response(template_file, context) @ensure_csrf_cookie @cache_if_anonymous def university_profile(request, org_id): """ Return the profile for the particular org_id. 404 if it's not valid. """ all_courses = modulestore().get_courses() valid_org_ids = set(c.org for c in all_courses).union(settings.VIRTUAL_UNIVERSITIES) if org_id not in valid_org_ids: raise Http404("University Profile not found for {0}".format(org_id)) # Only grab courses for this org... courses = get_courses_by_university(request.user, domain=request.META.get('HTTP_HOST'))[org_id] courses = sort_by_announcement(courses) context = dict(courses=courses, org_id=org_id) template_file = "university_profile/{0}.html".format(org_id).lower() return render_to_response(template_file, context) def render_notifications(request, course, notifications): context = { 'notifications': notifications, 'get_discussion_title': partial(get_discussion_title, request=request, course=course), 'course': course, } return render_to_string('courseware/notifications.html', context) @login_required def news(request, course_id): course = get_course_with_access(request.user, course_id, 'load') notifications = comment_client.get_notifications(request.user.id) context = { 'course': course, 'content': render_notifications(request, course, notifications), } return render_to_response('courseware/news.html', context) @login_required @cache_control(no_cache=True, no_store=True, must_revalidate=True) def progress(request, course_id, student_id=None): """ User progress. We show the grade bar and every problem score. Course staff are allowed to see the progress of students in their class. """ course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') if student_id is None or student_id == request.user.id: # always allowed to see your own profile student = request.user else: # Requesting access to a different student's profile if not staff_access: raise Http404 student = User.objects.get(id=int(student_id)) # NOTE: To make sure impersonation by instructor works, use # student instead of request.user in the rest of the function. # The pre-fetching of groups is done to make auth checks not require an # additional DB lookup (this kills the Progress page in particular). student = User.objects.prefetch_related("groups").get(id=student.id) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( course_id, student, course) courseware_summary = grades.progress_summary(student, request, course, student_module_cache) grade_summary = grades.grade(student, request, course, student_module_cache) if courseware_summary is None: #This means the student didn't have access to the course (which the instructor requested) raise Http404 context = {'course': course, 'courseware_summary': courseware_summary, 'grade_summary': grade_summary, 'staff_access': staff_access, } context.update() return render_to_response('courseware/progress.html', context)