utils.py 12.5 KB
Newer Older
1 2 3
"""
Common utility functions useful throughout the contentstore
"""
4
# pylint: disable=no-member
David Baumgold committed
5

6
import copy
cahrens committed
7
import logging
cahrens committed
8
import re
9 10
from datetime import datetime
from pytz import UTC
11 12

from django.conf import settings
David Baumgold committed
13
from django.utils.translation import ugettext as _
14
from django.core.urlresolvers import reverse
15 16
from django_comment_common.models import assign_default_role
from django_comment_common.utils import seed_permissions_roles
17 18

from xmodule.contentstore.content import StaticContent
19
from xmodule.modulestore import ModuleStoreEnum
20
from xmodule.modulestore.django import modulestore
21
from xmodule.modulestore.exceptions import ItemNotFoundError
22
from opaque_keys.edx.keys import UsageKey, CourseKey
23
from student.roles import CourseInstructorRole, CourseStaffRole
24 25
from student.models import CourseEnrollment
from student import auth
26

cahrens committed
27 28

log = logging.getLogger(__name__)
29

30
# In order to instantiate an open ended tab automatically, need to have this data
David Baumgold committed
31 32
OPEN_ENDED_PANEL = {"name": _("Open Ended Panel"), "type": "open_ended"}
NOTES_PANEL = {"name": _("My Notes"), "type": "notes"}
33 34
EDXNOTES_PANEL = {"name": _("Notes"), "type": "edxnotes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL, EDXNOTES_PANEL]])
Calen Pennington committed
35

36

37
def add_instructor(course_key, requesting_user, new_instructor):
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
    Adds given user as instructor and staff to the given course,
    after verifying that the requesting_user has permission to do so.
    """
    # can't use auth.add_users here b/c it requires user to already have Instructor perms in this course
    CourseInstructorRole(course_key).add_users(new_instructor)
    auth.add_users(requesting_user, CourseStaffRole(course_key), new_instructor)


def initialize_permissions(course_key, user_who_created_course):
    """
    Initializes a new course by enrolling the course creator as a student,
    and initializing Forum by seeding its permissions and assigning default roles.
    """
    # seed the forums
    seed_permissions_roles(course_key)

    # auto-enroll the course creator in the course so that "View Live" will work.
    CourseEnrollment.enroll(user_who_created_course, course_key)

    # set default forum roles (assign 'Student' role)
    assign_default_role(course_key, user_who_created_course)


def remove_all_instructors(course_key):
    """
64
    Removes all instructor and staff users from the given course.
65 66 67 68 69 70 71 72 73 74
    """
    staff_role = CourseStaffRole(course_key)
    staff_role.remove_users(*staff_role.users_with_role())
    instructor_role = CourseInstructorRole(course_key)
    instructor_role.remove_users(*instructor_role.users_with_role())


def delete_course_and_groups(course_key, user_id):
    """
    This deletes the courseware associated with a course_key as well as cleaning update_item
75 76
    the various user table stuff (groups, permissions, etc.)
    """
77
    module_store = modulestore()
78

79
    with module_store.bulk_operations(course_key):
80
        module_store.delete_course(course_key, user_id)
81

82 83 84
        print 'removing User permissions from course....'
        # in the django layer, we need to remove all the user permissions groups associated with this course
        try:
85
            remove_all_instructors(course_key)
86
        except Exception as err:
87
            log.error("Error in deleting course groups for {0}: {1}".format(course_key, err))
88

Calen Pennington committed
89

90
def get_lms_link_for_item(location, preview=False):
91 92 93 94 95 96
    """
    Returns an LMS link to the course with a jump_to to the provided location.

    :param location: the location to jump to
    :param preview: True if the preview version of LMS should be returned. Default value is false.
    """
97
    assert(isinstance(location, UsageKey))
98 99 100 101 102 103

    if settings.LMS_BASE is None:
        return None

    if preview:
        lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
104
    else:
105
        lms_base = settings.LMS_BASE
106

107
    return u"//{lms_base}/courses/{course_key}/jump_to/{location}".format(
108
        lms_base=lms_base,
109
        course_key=location.course_key.to_deprecated_string(),
110 111
        location=location.to_deprecated_string(),
    )
112

Calen Pennington committed
113

114
def get_lms_link_for_about_page(course_key):
cahrens committed
115 116 117
    """
    Returns the url to the course about page from the location tuple.
    """
118

119
    assert(isinstance(course_key, CourseKey))
120

121
    if settings.FEATURES.get('ENABLE_MKTG_SITE', False):
cahrens committed
122 123
        if not hasattr(settings, 'MKTG_URLS'):
            log.exception("ENABLE_MKTG_SITE is True, but MKTG_URLS is not defined.")
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
            return None

        marketing_urls = settings.MKTG_URLS

        # Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
        # but redirects exist from www.edx.org to get to the Drupal course about page URL.
        about_base = marketing_urls.get('ROOT', None)

        if about_base is None:
            log.exception('There is no ROOT defined in MKTG_URLS')
            return None

        # Strip off https:// (or http://) to be consistent with the formatting of LMS_BASE.
        about_base = re.sub(r"^https?://", "", about_base)

139 140 141
    elif settings.LMS_BASE is not None:
        about_base = settings.LMS_BASE
    else:
142
        return None
cahrens committed
143

144
    return u"//{about_base_url}/courses/{course_key}/about".format(
145
        about_base_url=about_base,
146
        course_key=course_key.to_deprecated_string()
147
    )
cahrens committed
148

Calen Pennington committed
149

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
# pylint: disable=invalid-name
def get_lms_link_for_certificate_web_view(user_id, course_key, mode):
    """
    Returns the url to the certificate web view.
    """
    assert isinstance(course_key, CourseKey)

    if settings.LMS_BASE is None:
        return None

    return u"//{certificate_web_base}/certificates/user/{user_id}/course/{course_id}?preview={mode}".format(
        certificate_web_base=settings.LMS_BASE,
        user_id=user_id,
        course_id=unicode(course_key),
        mode=mode
    )


168 169
def course_image_url(course):
    """Returns the image url for the course."""
170
    loc = StaticContent.compute_location(course.location.course_key, course.course_image)
171
    path = StaticContent.serialize_asset_key_with_slash(loc)
172 173
    return path

cahrens committed
174

175
# pylint: disable=invalid-name
176
def is_currently_visible_to_students(xblock):
177
    """
178 179
    Returns true if there is a published version of the xblock that is currently visible to students.
    This means that it has a release date in the past, and the xblock has not been set to staff only.
180 181 182
    """

    try:
183
        published = modulestore().get_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only)
184 185 186 187
    # If there's no published version then the xblock is clearly not visible
    except ItemNotFoundError:
        return False

188 189 190 191
    # If visible_to_staff_only is True, this xblock is not visible to students regardless of start date.
    if published.visible_to_staff_only:
        return False

192 193 194 195 196 197 198 199
    # Check start date
    if 'detached' not in published._class_tags and published.start is not None:
        return datetime.now(UTC) > published.start

    # No start date, so it's always visible
    return True


200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
def has_children_visible_to_specific_content_groups(xblock):
    """
    Returns True if this xblock has children that are limited to specific content groups.
    Note that this method is not recursive (it does not check grandchildren).
    """
    if not xblock.has_children:
        return False

    for child in xblock.get_children():
        if is_visible_to_specific_content_groups(child):
            return True

    return False


def is_visible_to_specific_content_groups(xblock):
    """
    Returns True if this xblock has visibility limited to specific content groups.
    """
    if not xblock.group_access:
        return False
    for __, value in xblock.group_access.iteritems():
        # value should be a list of group IDs. If it is an empty list or None, the xblock is visible
        # to all groups in that particular partition. So if value is a truthy value, the xblock is
        # restricted in some way.
        if value:
            return True
    return False


230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
def find_release_date_source(xblock):
    """
    Finds the ancestor of xblock that set its release date.
    """

    # Stop searching at the section level
    if xblock.category == 'chapter':
        return xblock

    parent_location = modulestore().get_parent_location(xblock.location,
                                                        revision=ModuleStoreEnum.RevisionOption.draft_preferred)
    # Orphaned xblocks set their own release date
    if not parent_location:
        return xblock

    parent = modulestore().get_item(parent_location)
    if parent.start != xblock.start:
        return xblock
    else:
        return find_release_date_source(parent)


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 279 280 281 282 283 284 285 286 287 288 289
def find_staff_lock_source(xblock):
    """
    Returns the xblock responsible for setting this xblock's staff lock, or None if the xblock is not staff locked.
    If this xblock is explicitly locked, return it, otherwise find the ancestor which sets this xblock's staff lock.
    """

    # Stop searching if this xblock has explicitly set its own staff lock
    if xblock.fields['visible_to_staff_only'].is_set_on(xblock):
        return xblock

    # Stop searching at the section level
    if xblock.category == 'chapter':
        return None

    parent_location = modulestore().get_parent_location(xblock.location,
                                                        revision=ModuleStoreEnum.RevisionOption.draft_preferred)
    # Orphaned xblocks set their own staff lock
    if not parent_location:
        return None

    parent = modulestore().get_item(parent_location)
    return find_staff_lock_source(parent)


def ancestor_has_staff_lock(xblock, parent_xblock=None):
    """
    Returns True iff one of xblock's ancestors has staff lock.
    Can avoid mongo query by passing in parent_xblock.
    """
    if parent_xblock is None:
        parent_location = modulestore().get_parent_location(xblock.location,
                                                            revision=ModuleStoreEnum.RevisionOption.draft_preferred)
        if not parent_location:
            return False
        parent_xblock = modulestore().get_item(parent_location)
    return parent_xblock.visible_to_staff_only


290
def add_extra_panel_tab(tab_type, course):
Vik Paruchuri committed
291
    """
292 293
    Used to add the panel tab to a course if it does not exist.
    @param tab_type: A string representing the tab type.
Vik Paruchuri committed
294 295 296
    @param course: A course object from the modulestore.
    @return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
    """
Don Mitchell committed
297
    # Copy course tabs
298 299
    course_tabs = copy.copy(course.tabs)
    changed = False
Don Mitchell committed
300
    # Check to see if open ended panel is defined in the course
301

302 303
    tab_panel = EXTRA_TAB_PANELS.get(tab_type)
    if tab_panel not in course_tabs:
Don Mitchell committed
304
        # Add panel to the tabs if it is not defined
305
        course_tabs.append(tab_panel)
306 307
        changed = True
    return changed, course_tabs
308

Chris Dodge committed
309

310
def remove_extra_panel_tab(tab_type, course):
311
    """
312 313
    Used to remove the panel tab from a course if it exists.
    @param tab_type: A string representing the tab type.
314 315 316
    @param course: A course object from the modulestore.
    @return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
    """
Don Mitchell committed
317
    # Copy course tabs
318 319
    course_tabs = copy.copy(course.tabs)
    changed = False
Don Mitchell committed
320
    # Check to see if open ended panel is defined in the course
321 322 323

    tab_panel = EXTRA_TAB_PANELS.get(tab_type)
    if tab_panel in course_tabs:
Don Mitchell committed
324
        # Add panel to the tabs if it is not defined
325
        course_tabs = [ct for ct in course_tabs if ct != tab_panel]
326 327
        changed = True
    return changed, course_tabs
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347


def reverse_url(handler_name, key_name=None, key_value=None, kwargs=None):
    """
    Creates the URL for the given handler.
    The optional key_name and key_value are passed in as kwargs to the handler.
    """
    kwargs_for_reverse = {key_name: unicode(key_value)} if key_name else None
    if kwargs:
        kwargs_for_reverse.update(kwargs)
    return reverse('contentstore.views.' + handler_name, kwargs=kwargs_for_reverse)


def reverse_course_url(handler_name, course_key, kwargs=None):
    """
    Creates the URL for handlers that use course_keys as URL parameters.
    """
    return reverse_url(handler_name, 'course_key_string', course_key, kwargs)


348 349 350 351 352 353 354
def reverse_library_url(handler_name, library_key, kwargs=None):
    """
    Creates the URL for handlers that use library_keys as URL parameters.
    """
    return reverse_url(handler_name, 'library_key_string', library_key, kwargs)


355 356 357 358 359
def reverse_usage_url(handler_name, usage_key, kwargs=None):
    """
    Creates the URL for handlers that use usage_keys as URL parameters.
    """
    return reverse_url(handler_name, 'usage_key_string', usage_key, kwargs)