tabs.py 14.1 KB
Newer Older
Victor Shnayder committed
1 2 3 4 5 6 7 8 9 10 11 12 13
"""
Tabs configuration.  By the time the tab is being rendered, it's just a name,
link, and css class (CourseTab tuple).  Tabs are specified in course policy.
Each tab has a type, and possibly some type-specific parameters.

To add a new tab type, add a TabImpl to the VALID_TAB_TYPES dict below--it will
contain a validation function that checks whether config for the tab type is
valid, and a generator function that takes the config, user, and course, and
actually generates the CourseTab.
"""

from collections import namedtuple
import logging
14
import json
Victor Shnayder committed
15 16 17 18

from django.conf import settings
from django.core.urlresolvers import reverse

19 20
from fs.errors import ResourceNotFoundError

Victor Shnayder committed
21 22
from courseware.access import has_access

23
from lxml.html import rewrite_links
24
from .module_render import get_module
25 26 27 28 29 30
from courseware.access import has_access
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.x_module import XModule
from student.models import unique_id_for_user
Calen Pennington committed
31
from courseware.model_data import ModelDataCache
32

33
from open_ended_grading import open_ended_notifications
34

Victor Shnayder committed
35 36
log = logging.getLogger(__name__)

Calen Pennington committed
37

Victor Shnayder committed
38 39 40 41 42 43
class InvalidTabsException(Exception):
    """
    A complaint about invalid tabs.
    """
    pass

44 45
CourseTabBase = namedtuple('CourseTab', 'name link is_active has_img img')

Calen Pennington committed
46

47 48
def CourseTab(name, link, is_active, has_img=False, img=""):
    return CourseTabBase(name, link, is_active, has_img, img)
Victor Shnayder committed
49 50 51 52 53 54 55

# encapsulate implementation for a tab:
#  - a validation function: takes the config dict and raises
#    InvalidTabsException if required fields are missing or otherwise
#    wrong.  (e.g. "is there a 'name' field?).  Validators can assume
#    that the type field is valid.
#
56
#  - a function that takes a config, a user, and a course, an active_page and
Victor Shnayder committed
57 58 59 60 61 62 63 64 65 66 67 68 69
#    return a list of CourseTabs.  (e.g. "return a CourseTab with specified
#    name, link to courseware, and is_active=True/False").  The function can
#    assume that it is only called with configs of the appropriate type that
#    have passed the corresponding validator.
TabImpl = namedtuple('TabImpl', 'validator generator')


#####  Generators for various tabs.

def _courseware(tab, user, course, active_page):
    link = reverse('courseware', args=[course.id])
    return [CourseTab('Courseware', link, active_page == "courseware")]

Calen Pennington committed
70

Victor Shnayder committed
71 72 73 74
def _course_info(tab, user, course, active_page):
    link = reverse('info', args=[course.id])
    return [CourseTab(tab['name'], link, active_page == "info")]

Calen Pennington committed
75

Victor Shnayder committed
76 77 78 79 80 81
def _progress(tab, user, course, active_page):
    if user.is_authenticated():
        link = reverse('progress', args=[course.id])
        return [CourseTab(tab['name'], link, active_page == "progress")]
    return []

Calen Pennington committed
82

Victor Shnayder committed
83 84 85 86 87 88
def _wiki(tab, user, course, active_page):
    if settings.WIKI_ENABLED:
        link = reverse('course_wiki', args=[course.id])
        return [CourseTab(tab['name'], link, active_page == 'wiki')]
    return []

Calen Pennington committed
89

Victor Shnayder committed
90 91 92 93 94 95 96
def _discussion(tab, user, course, active_page):
    """
    This tab format only supports the new Berkeley discussion forums.
    """
    if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
        link = reverse('django_comment_client.forum.views.forum_form_discussion',
                              args=[course.id])
Calen Pennington committed
97
        return [CourseTab(tab['name'], link, active_page == 'discussion')]
Victor Shnayder committed
98 99
    return []

Calen Pennington committed
100

Victor Shnayder committed
101 102 103 104
def _external_link(tab, user, course, active_page):
    # external links are never active
    return [CourseTab(tab['name'], tab['link'], False)]

Calen Pennington committed
105

Victor Shnayder committed
106 107 108
def _static_tab(tab, user, course, active_page):
    link = reverse('static_tab', args=[course.id, tab['url_slug']])
    active_str = 'static_tab_{0}'.format(tab['url_slug'])
Calen Pennington committed
109
    return [CourseTab(tab['name'], link, active_page == active_str)]
Victor Shnayder committed
110

Victor Shnayder committed
111 112 113 114 115 116 117 118

def _textbooks(tab, user, course, active_page):
    """
    Generates one tab per textbook.  Only displays if user is authenticated.
    """
    if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
        # since there can be more than one textbook, active_page is e.g. "book/0".
        return [CourseTab(textbook.title, reverse('book', args=[course.id, index]),
Calen Pennington committed
119
                          active_page == "textbook/{0}".format(index))
Victor Shnayder committed
120 121 122
                for index, textbook in enumerate(course.textbooks)]
    return []

Brian Wilson committed
123 124 125 126 127 128 129 130 131 132
def _pdf_textbooks(tab, user, course, active_page):
    """
    Generates one tab per textbook.  Only displays if user is authenticated.
    """
    if user.is_authenticated():
        # since there can be more than one textbook, active_page is e.g. "book/0".
        return [CourseTab(textbook['tab_title'], reverse('pdf_book', args=[course.id, index]),
                          active_page == "pdftextbook/{0}".format(index))
                for index, textbook in enumerate(course.pdf_textbooks)]
    return []
133

134 135 136 137 138 139 140 141 142 143 144
def _html_textbooks(tab, user, course, active_page):
    """
    Generates one tab per textbook.  Only displays if user is authenticated.
    """
    if user.is_authenticated():
        # since there can be more than one textbook, active_page is e.g. "book/0".
        return [CourseTab(textbook['tab_title'], reverse('html_book', args=[course.id, index]),
                          active_page == "htmltextbook/{0}".format(index))
                for index, textbook in enumerate(course.html_textbooks)]
    return []

145 146 147
def _staff_grading(tab, user, course, active_page):
    if has_access(user, course, 'staff'):
        link = reverse('staff_grading', args=[course.id])
148

149
        tab_name = "Staff grading"
150

151
        notifications  = open_ended_notifications.staff_grading_notifications(course, user)
152 153
        pending_grading = notifications['pending_grading']
        img_path = notifications['img_path']
154 155 156

        tab = [CourseTab(tab_name, link, active_page == "staff_grading", pending_grading, img_path)]
        return tab
157 158
    return []

Calen Pennington committed
159

160
def _peer_grading(tab, user, course, active_page):
161

162 163 164
    if user.is_authenticated():
        link = reverse('peer_grading', args=[course.id])
        tab_name = "Peer grading"
165

166
        notifications = open_ended_notifications.peer_grading_notifications(course, user)
167 168
        pending_grading = notifications['pending_grading']
        img_path = notifications['img_path']
169 170 171 172

        tab = [CourseTab(tab_name, link, active_page == "peer_grading", pending_grading, img_path)]
        return tab
    return []
173

Calen Pennington committed
174

175
def _combined_open_ended_grading(tab, user, course, active_page):
176
    if user.is_authenticated():
Vik Paruchuri committed
177 178
        link = reverse('open_ended_notifications', args=[course.id])
        tab_name = "Open Ended Panel"
179

180
        notifications  = open_ended_notifications.combined_notifications(course, user)
181 182
        pending_grading = notifications['pending_grading']
        img_path = notifications['img_path']
183

184
        tab = [CourseTab(tab_name, link, active_page == "open_ended", pending_grading, img_path)]
185 186 187
        return tab
    return []

188 189
def _notes_tab(tab, user, course, active_page):
    if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'):
190
        link = reverse('notes', args=[course.id])
191
        return [CourseTab(tab['name'], link, active_page == 'notes')]
192
    return []
193

Victor Shnayder committed
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
#### Validators


def key_checker(expected_keys):
    """
    Returns a function that checks that specified keys are present in a dict
    """
    def check(d):
        for k in expected_keys:
            if k not in d:
                raise InvalidTabsException("Key {0} not present in {1}"
                                           .format(k, d))
    return check


need_name = key_checker(['name'])

Calen Pennington committed
211

Victor Shnayder committed
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
def null_validator(d):
    """
    Don't check anything--use for tabs that don't need any params. (e.g. textbook)
    """
    pass

##### The main tab config dict.

# type -> TabImpl
VALID_TAB_TYPES = {
    'courseware': TabImpl(null_validator, _courseware),
    'course_info': TabImpl(need_name, _course_info),
    'wiki': TabImpl(need_name, _wiki),
    'discussion': TabImpl(need_name, _discussion),
    'external_link': TabImpl(key_checker(['name', 'link']), _external_link),
    'textbooks': TabImpl(null_validator, _textbooks),
Brian Wilson committed
228
    'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks),
Brian Wilson committed
229
    'html_textbooks': TabImpl(null_validator, _html_textbooks),
Victor Shnayder committed
230
    'progress': TabImpl(need_name, _progress),
Victor Shnayder committed
231
    'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab),
232
    'peer_grading': TabImpl(null_validator, _peer_grading),
233
    'staff_grading': TabImpl(null_validator, _staff_grading),
234
    'open_ended': TabImpl(null_validator, _combined_open_ended_grading),
235
    'notes': TabImpl(null_validator, _notes_tab)
Victor Shnayder committed
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
    }


### External interface below this.

def validate_tabs(course):
    """
    Check that the tabs set for the specified course is valid.  If it
    isn't, raise InvalidTabsException with the complaint.

    Specific rules checked:
    - if no tabs specified, that's fine
    - if tabs specified, first two must have type 'courseware' and 'course_info', in that order.
    - All the tabs must have a type in VALID_TAB_TYPES.

    """
    tabs = course.tabs
    if tabs is None:
        return

    if len(tabs) < 2:
        raise InvalidTabsException("Expected at least two tabs.  tabs: '{0}'".format(tabs))
    if tabs[0]['type'] != 'courseware':
        raise InvalidTabsException(
            "Expected first tab to have type 'courseware'.  tabs: '{0}'".format(tabs))
    if tabs[1]['type'] != 'course_info':
        raise InvalidTabsException(
            "Expected second tab to have type 'course_info'.  tabs: '{0}'".format(tabs))
    for t in tabs:
        if t['type'] not in VALID_TAB_TYPES:
            raise InvalidTabsException("Unknown tab type {0}. Known types: {1}"
                                       .format(t['type'], VALID_TAB_TYPES))
        # the type-specific validator checks the rest of the tab config
        VALID_TAB_TYPES[t['type']].validator(t)

    # Possible other checks: make sure tabs that should only appear once (e.g. courseware)
    # are actually unique (otherwise, will break active tag code)


def get_course_tabs(user, course, active_page):
    """
    Return the tabs to show a particular user, as a list of CourseTab items.
    """
Calen Pennington committed
279
    if not hasattr(course, 'tabs') or not course.tabs:
Victor Shnayder committed
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
        return get_default_tabs(user, course, active_page)

    # TODO (vshnayder): There needs to be a place to call this right after course
    # load, but not from inside xmodule, since that doesn't (and probably
    # shouldn't) know about the details of what tabs are supported, etc.
    validate_tabs(course)

    tabs = []
    for tab in course.tabs:
        # expect handlers to return lists--handles things that are turned off
        # via feature flags, and things like 'textbook' which might generate
        # multiple tabs.
        gen = VALID_TAB_TYPES[tab['type']].generator
        tabs.extend(gen(tab, user, course, active_page))

    # Instructor tab is special--automatically added if user is staff for the course
    if has_access(user, course, 'staff'):
        tabs.append(CourseTab('Instructor',
                              reverse('instructor_dashboard', args=[course.id]),
                              active_page == 'instructor'))
    return tabs


303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
def get_discussion_link(course):
    """
    Return the URL for the discussion tab for the given `course`.

    If they have a discussion link specified, use that even if we disable
    discussions. Disabling discsussions is mostly a server safety feature at
    this point, and we don't need to worry about external sites. Otherwise,
    if the course has a discussion tab or uses the default tabs, return the
    discussion view URL. Otherwise, return None to indicate the lack of a
    discussion tab.
    """
    if course.discussion_link:
        return course.discussion_link
    elif not settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
        return None
    elif hasattr(course, 'tabs') and course.tabs and not any([tab['type'] == 'discussion' for tab in course.tabs]):
        return None
    else:
        return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id])


Victor Shnayder committed
324 325 326 327 328 329 330 331 332 333
def get_default_tabs(user, course, active_page):

    # When calling the various _tab methods, can omit the 'type':'blah' from the
    # first arg, since that's only used for dispatch
    tabs = []
    tabs.extend(_courseware({''}, user, course, active_page))
    tabs.extend(_course_info({'name': 'Course Info'}, user, course, active_page))

    if hasattr(course, 'syllabus_present') and course.syllabus_present:
        link = reverse('syllabus', args=[course.id])
Calen Pennington committed
334
        tabs.append(CourseTab('Syllabus', link, active_page == 'syllabus'))
Victor Shnayder committed
335 336 337

    tabs.extend(_textbooks({}, user, course, active_page))

338 339 340
    discussion_link = get_discussion_link(course)
    if discussion_link:
        tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion'))
Victor Shnayder committed
341 342 343 344 345 346 347 348

    tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))

    if user.is_authenticated() and not course.hide_progress_tab:
        tabs.extend(_progress({'name': 'Progress'}, user, course, active_page))

    if has_access(user, course, 'staff'):
        link = reverse('instructor_dashboard', args=[course.id])
Calen Pennington committed
349
        tabs.append(CourseTab('Instructor', link, active_page == 'instructor'))
Victor Shnayder committed
350 351

    return tabs
Victor Shnayder committed
352

Calen Pennington committed
353

354
def get_static_tab_by_slug(course, tab_slug):
Victor Shnayder committed
355 356 357 358
    """
    Look for a tab with type 'static_tab' and the specified 'tab_slug'.  Returns
    the tab (a config dict), or None if not found.
    """
359 360 361
    if course.tabs is None:
        return None
    for tab in course.tabs:
362
        # The validation code checks that these exist.
Victor Shnayder committed
363 364 365 366 367
        if tab['type'] == 'static_tab' and tab['url_slug'] == tab_slug:
            return tab

    return None

Calen Pennington committed
368

Calen Pennington committed
369
def get_static_tab_contents(request, course, tab):
370

371
    loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug'])
Calen Pennington committed
372 373 374
    model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course.id,
        request.user, modulestore().get_instance(course.id, loc), depth=0)
    tab_module = get_module(request.user, request, loc, model_data_cache, course.id)
375

376 377 378 379 380 381 382
    logging.debug('course_module = {0}'.format(tab_module))

    html = ''

    if tab_module is not None:
        html = tab_module.get_html()

383
    return html