Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
14df787
Start of studio support for libraries: two views, nav menu link, urlc…
bradenmacdonald Oct 7, 2014
fac5060
A busted read-only view of library content, based on container template
bradenmacdonald Oct 7, 2014
46edb28
Include library version info in the JSON API view
bradenmacdonald Oct 7, 2014
063ff7e
Stop Studio REST API methods from stripping branch data.
bradenmacdonald Oct 7, 2014
1d80203
Fix deleting blocks in studio: split mongo's draft awareness was over…
bradenmacdonald Oct 8, 2014
74bf813
Minor format/comment cleanups
bradenmacdonald Oct 9, 2014
ed57db2
Support for creating new content libraries on existing courses
bradenmacdonald Oct 9, 2014
ccfbb64
Revert change that seems unnecessary
bradenmacdonald Oct 9, 2014
5ba66aa
Starting point for a LibraryContent XBlock
bradenmacdonald Oct 10, 2014
a6a8c32
Display the status of a LibraryContentXBlock in the studio preview.
bradenmacdonald Oct 10, 2014
7e94151
Minor: Don't hide the due date
bradenmacdonald Oct 11, 2014
7dc648f
It's alive! Can now import content from a library.
bradenmacdonald Oct 11, 2014
ce007c8
Fix bug in split: was impossible to re-use a definition without creat…
bradenmacdonald Oct 11, 2014
0abd490
Create consistent block_id for child blocks imported from a library
bradenmacdonald Oct 14, 2014
2aab264
Can now randomly select up to N blocks from a library
bradenmacdonald Oct 15, 2014
d2b8348
Show a debug button in the LMS to help test library content randomiza…
bradenmacdonald Oct 15, 2014
edc37a4
Minor: work around inability to refresh the XBlock when synchronizing…
bradenmacdonald Oct 15, 2014
234121c
Minor fix for "first" mode
bradenmacdonald Oct 16, 2014
4aa0361
Clarify graded vs. scored. Fix inheritance so "due_date" field is gon…
bradenmacdonald Oct 16, 2014
1e99cd0
Hide LibraryContent's children from the grading system and progress v…
bradenmacdonald Oct 16, 2014
b368c3e
Support for graded LibraryContent blocks
bradenmacdonald Oct 16, 2014
40de949
Fix graph on course progress tab.
bradenmacdonald Oct 16, 2014
6bc05a5
Update StudentModule type enum to reflect actual usage. Probably shou…
bradenmacdonald Oct 16, 2014
8a4f149
Fix: acid XBlock was broken in studio
bradenmacdonald Oct 17, 2014
7be9b67
Support for new LibraryLocator in LibraryContentBlocks
bradenmacdonald Oct 21, 2014
420a936
Support for fetching & editing blocks inside a library accessed with …
bradenmacdonald Oct 21, 2014
a1043e7
Reverse earlier dubious changes to CMS 'item.py' views, no longer needed
bradenmacdonald Oct 21, 2014
e5d8680
Fixes so library view works on libraries that don't have course-relat…
bradenmacdonald Oct 22, 2014
d2a114c
List libraries separately on the studio homepage
bradenmacdonald Oct 22, 2014
89112b9
Ability to create new libraries from studio homepage
bradenmacdonald Oct 23, 2014
18b0740
Cleanup: Make locator type less explicit
bradenmacdonald Oct 24, 2014
d7ba4ff
Cleanups and fixes
bradenmacdonald Oct 24, 2014
2060664
New, simple approach to grading
bradenmacdonald Oct 24, 2014
e93af30
Temporary: specify the new opaque keys package w/ LibraryLocator
bradenmacdonald Oct 24, 2014
adc398c
Minor cleanups based on Eugeny's PR feedback
bradenmacdonald Oct 27, 2014
4e841ef
Move library branch handling to split_draft / _map_revision_to_branch
bradenmacdonald Oct 29, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ def reverse_course_url(handler_name, course_key, kwargs=None):
return reverse_url(handler_name, 'course_key_string', course_key, kwargs)


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)


def reverse_usage_url(handler_name, usage_key, kwargs=None):
"""
Creates the URL for handlers that use usage_keys as URL parameters.
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .helpers import *
from .item import *
from .import_export import *
from .library import *
from .preview import *
from .public import *
from .export_git import *
Expand Down
25 changes: 25 additions & 0 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
add_extra_panel_tab,
remove_extra_panel_tab,
reverse_course_url,
reverse_library_url,
reverse_usage_url,
reverse_url,
remove_all_instructors,
Expand All @@ -56,6 +57,7 @@
ADVANCED_COMPONENT_TYPES,
)
from contentstore.tasks import rerun_course
from .library import LIBRARIES_ENABLED
from .item import create_xblock_info
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
Expand Down Expand Up @@ -340,6 +342,14 @@ def _accessible_courses_list_from_groups(request):
return courses_list.values(), in_process_course_actions


def _accessible_libraries_list(user):
"""
List all libraries available to the logged in user by iterating through all libraries
"""
# No need to worry about ErrorDescriptors - split's get_libraries() never returns them.
return [lib for lib in modulestore().get_libraries() if has_course_access(user, lib.location)]


@login_required
@ensure_csrf_cookie
def course_listing(request):
Expand All @@ -359,6 +369,8 @@ def course_listing(request):
# so fallback to iterating through all courses
courses, in_process_course_actions = _accessible_courses_list(request)

libraries = _accessible_libraries_list(request.user) if LIBRARIES_ENABLED else []

def format_course_for_view(course):
"""
Return a dict of the data which the view requires for each course
Expand Down Expand Up @@ -392,6 +404,18 @@ def format_in_process_course_view(uca):
}) if uca.state == CourseRerunUIStateManager.State.FAILED else ''
}

def format_library_for_view(library):
"""
Return a dict of the data which the view requires for each library
"""
return {
'display_name': library.display_name,
'library_key': unicode(library.location.library_key),
'url': reverse_library_url('library_handler', unicode(library.location.library_key)),
'org': library.display_org_with_default,
'number': library.display_number_with_default,
}

# remove any courses in courses that are also in the in_process_course_actions list
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
courses = [
Expand All @@ -405,6 +429,7 @@ def format_in_process_course_view(uca):
return render_to_response('index.html', {
'courses': courses,
'in_process_course_actions': in_process_course_actions,
'libraries': [format_library_for_view(lib) for lib in libraries],
'user': request.user,
'request_course_creator_url': reverse('contentstore.views.request_course_creator'),
'course_creator_status': _get_course_creator_status(request.user),
Expand Down
5 changes: 4 additions & 1 deletion cms/djangoapps/contentstore/views/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from edxmako.shortcuts import render_to_string, render_to_response
from xblock.core import XBlock
from xmodule.modulestore.django import modulestore
from contentstore.utils import reverse_course_url, reverse_usage_url
from contentstore.utils import reverse_course_url, reverse_library_url, reverse_usage_url

__all__ = ['edge', 'event', 'landing']

Expand Down Expand Up @@ -106,6 +106,9 @@ def xblock_studio_url(xblock, parent_xblock=None):
url=reverse_course_url('course_handler', xblock.location.course_key),
usage_key=urllib.quote(unicode(xblock.location))
)
elif category == 'library':
library_key = xblock.location.course_key
return reverse_library_url('library_handler', library_key)
else:
return reverse_usage_url('container_handler', xblock.location)

Expand Down
19 changes: 13 additions & 6 deletions cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.runtime import handler_url, local_resource_url
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locator import LibraryUsageLocator

__all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler']

Expand Down Expand Up @@ -650,7 +651,9 @@ def _get_module_info(xblock, rewrite_static_links=True):
)

# Pre-cache has changes for the entire course because we'll need it for the ancestor info
modulestore().has_changes(modulestore().get_course(xblock.location.course_key, depth=None))
# Except library blocks which don't use draft/publish
if not isinstance(xblock.location, LibraryUsageLocator):
modulestore().has_changes(modulestore().get_courselike(xblock.location.course_key, depth=None))

# Note that children aren't being returned until we have a use case.
return create_xblock_info(xblock, data=data, metadata=own_metadata(xblock), include_ancestor_info=True)
Expand Down Expand Up @@ -691,12 +694,16 @@ def safe_get_username(user_id):

return None

is_library_block = isinstance(xblock.location, LibraryUsageLocator)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same code on line 655 - might make sense to extract method / function.

is_xblock_unit = is_unit(xblock, parent_xblock)
# this should not be calculated for Sections and Subsections on Unit page
has_changes = modulestore().has_changes(xblock) if (is_xblock_unit or course_outline) else None
# this should not be calculated for Sections and Subsections on Unit page or for library blocks
has_changes = modulestore().has_changes(xblock) if (is_xblock_unit or course_outline) and not is_library_block else None

if graders is None:
graders = CourseGradingModel.fetch(xblock.location.course_key).graders
if xblock.category != "library":
graders = CourseGradingModel.fetch(xblock.location.course_key).graders

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald I have a subtle feeling here that it should be responsibility of CourseGradingModel to handle this. On the other hand, if fetch returns None or throws when given xblock.category == 'library' it might require pretty radical changes. So, if you already tried moving this logic to CourseGradingModel and it turned out to be problematic - I'm perfectly fine with that. Otherwise, could you please investigate this a bit, it should result in a slightly better separation of concerns? :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@e-kolpakov Yes, fetch throws when given a library instead of a course, and changing that has unknown implications. I think it's actually better separation of concerns this way - because CourseGradingModel doesn't know anything about libraries, and this one line of code that we're discussing was the only line I found that would try to ask CourseGradingModel about libraries. So I think it's better to say "don't ask CourseGradingModel to give you the graders for a library" (this is the only line of code affected) than it is to make a bunch of changes to CourseGradingModel to make it aware of libraries.

else:
graders = []

# Compute the child info first so it can be included in aggregate information for the parent
should_visit_children = include_child_info and (course_outline and not is_xblock_unit or not course_outline)
Expand All @@ -716,15 +723,15 @@ def safe_get_username(user_id):
visibility_state = _compute_visibility_state(xblock, child_info, is_xblock_unit and has_changes)
else:
visibility_state = None
published = modulestore().has_published_version(xblock)
published = modulestore().has_published_version(xblock) if not is_library_block else None

xblock_info = {
"id": unicode(xblock.location),
"display_name": xblock.display_name_with_default,
"category": xblock.category,
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
"published": published,
"published_on": get_default_time_display(xblock.published_on) if xblock.published_on else None,
"published_on": get_default_time_display(xblock.published_on) if published and xblock.published_on else None,
"studio_url": xblock_studio_url(xblock, parent_xblock),
"released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date,
Expand Down
169 changes: 169 additions & 0 deletions cms/djangoapps/contentstore/views/library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""
Views related to content libraries.
A content library is a structure containing XBlocks which can be re-used in the
multiple courses.
"""
from __future__ import absolute_import

import json
import logging

from contentstore.views.item import create_xblock_info
from contentstore.utils import reverse_library_url
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, Http404
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.conf import settings
from django.utils.translation import ugettext as _
from django_future.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator, LibraryUsageLocator
from xmodule.library_module import LibraryDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore

from .access import has_course_access
from .component import get_component_templates
from student.roles import CourseCreatorRole
from student import auth
from util.json_request import expect_json, JsonResponse

__all__ = ['library_handler']

log = logging.getLogger(__name__)

LIBRARIES_ENABLED = settings.FEATURES.get('ENABLE_CONTENT_LIBRARIES', False)


@login_required
@ensure_csrf_cookie
def library_handler(request, library_key_string=None):
"""
RESTful interface to most content library related functionality.
"""
if not LIBRARIES_ENABLED:
raise Http404 # Should never happen because we test the feature in urls.py also

response_format = 'html'
if request.REQUEST.get('format', 'html') == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'text/html'):
response_format = 'json'

if library_key_string:
library_key = CourseKey.from_string(library_key_string)
if not isinstance(library_key, LibraryLocator):
raise Http404 # This is not a library
if not has_course_access(request.user, library_key):
raise PermissionDenied()

try:
library = modulestore().get_library(library_key)
if library is None:
raise ItemNotFoundError # Inconsistency: mixed modulestore returns None, whereas split raises exception
except ItemNotFoundError:
raise Http404

if not isinstance(library, LibraryDescriptor):
return HttpResponseBadRequest("Key specified is not a library.")

if request.method == 'GET':
return library_blocks_view(request, library, response_format)
return HttpResponseNotAllowed(['GET'])

elif request.method == 'POST':
# Create a new library:
return _create_library(request)
elif request.method == 'GET':
# List all accessible libraries:
lib_info = [
{
"display_name": lib.display_name,
"library_key": unicode(lib.location.library_key),
}
for lib in modulestore().get_libraries()
if has_course_access(request.user, lib.location.library_key)
]
return JsonResponse(lib_info)
else:
return HttpResponseNotAllowed(['GET', 'POST'])


@expect_json
def _create_library(request):
"""
Helper method for creating a new library.
"""
if not auth.has_access(request.user, CourseCreatorRole()):
raise PermissionDenied()
try:
org = request.json['org']
library = request.json.get('number', None)
if library is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equivalent to library = request.json.get('library', request.json.get('number', None))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's actually equivalent? I think the equivalent code would need to be:

library = request.json.get('number', request.json.get('library', None))
if library is None:
    raise KeyError

... because I'm relying on the KeyError to arise from request.json['library'] (Note that .get() never raises KeyError)

library = request.json['library']
display_name = request.json['display_name']
store = modulestore()
with store.default_store(ModuleStoreEnum.Type.split):
new_lib = store.create_library(
org=org,
library=library,
user_id=request.user.id,
fields={"display_name": display_name},
)
except KeyError as error:
return JsonResponse({
"ErrMsg": _("Unable to create library - missing expected JSON key '{err}'").format(err=error.message)}
)
except InvalidKeyError as error:
return JsonResponse({
"ErrMsg": _("Unable to create library - invalid data.\n\n{err}").format(name=display_name, err=error.message)}
)
except DuplicateCourseError as error:
return JsonResponse({
"ErrMsg": _("Unable to create library - one already exists with that key.\n\n{err}").format(err=error.message)}
)

lib_key_str = unicode(new_lib.location.library_key)
return JsonResponse({
'url': reverse_library_url('library_handler', lib_key_str),
'library_key': lib_key_str,
})


def library_blocks_view(request, library, response_format):
"""
The main view of a course's content library.
Shows all the XBlocks in the library, and allows adding/editing/deleting
them.
Can be called with response_format="json" to get a JSON-formatted list of
the XBlocks in the library along with library metadata.
"""
children = library.children
if response_format == "json":
# The JSON response for this request is short and sweet:
prev_version = library.runtime.course_entry.structure['previous_version']
return JsonResponse({
"display_name": library.display_name,
"library_id": unicode(library.location.course_key), # library.course_id raises UndefinedContext - fix?
"version": unicode(library.runtime.course_entry.course_key.version),
"previous_version": unicode(prev_version) if prev_version else None,
"blocks": [unicode(x) for x in children],
})

xblock_info = create_xblock_info(library, include_ancestor_info=False, graders=[])

component_templates = get_component_templates(library)

assert isinstance(library.location.library_key, LibraryLocator)
assert isinstance(library.location, LibraryUsageLocator)

return render_to_response('library.html', {
'context_library': library,
'action': 'view',
'xblock': library,
'xblock_locator': library.location,
'unit': None,
'component_templates': json.dumps(component_templates),
'xblock_info': xblock_info,
})
Loading