def test_get_item(self):
        '''
        get_item(blocklocator)
        '''
        # positive tests of various forms
        locator = BlockUsageLocator(version_guid=self.GUID_D1, usage_id='head12345')
        block = modulestore().get_item(locator)
        self.assertIsInstance(block, CourseDescriptor)

        locator = BlockUsageLocator(course_id='GreekHero', usage_id='head12345', branch='draft')
        block = modulestore().get_item(locator)
        self.assertEqual(block.location.course_id, "GreekHero")
        # look at this one in detail
        self.assertEqual(len(block.tabs), 6, "wrong number of tabs")
        self.assertEqual(block.display_name, "The Ancient Greek Hero")
        self.assertEqual(block.advertised_start, "Fall 2013")
        self.assertEqual(len(block.children), 3)
        self.assertEqual(block.definition_locator.definition_id, "head12345_12")
        # check dates and graders--forces loading of descriptor
        self.assertEqual(block.edited_by, "*****@*****.**")
        self.assertDictEqual(
            block.grade_cutoffs, {"Pass": 0.45},
        )

        # try to look up other branches
        self.assertRaises(ItemNotFoundError,
                          modulestore().get_item,
                          BlockUsageLocator(course_id=locator.as_course_locator(),
                                            usage_id=locator.usage_id,
                                            branch='published'))
        locator.branch = 'draft'
        self.assertIsInstance(
            modulestore().get_item(locator),
            CourseDescriptor
        )
Example #2
0
    def test_old_location_helpers(self):
        """
        Test the functions intended to help with the conversion from old locations to locators
        """
        location_tuple = ('i4x', 'mit', 'eecs.6002x', 'course', 't3_2013')
        location = Location(location_tuple)
        self.assertEqual(location, Locator.to_locator_or_location(location))
        self.assertEqual(location, Locator.to_locator_or_location(location_tuple))
        self.assertEqual(location, Locator.to_locator_or_location(list(location_tuple)))
        self.assertEqual(location, Locator.to_locator_or_location(location.dict()))

        locator = BlockUsageLocator(package_id='foo.bar', branch='alpha', block_id='deep')
        self.assertEqual(locator, Locator.to_locator_or_location(locator))
        self.assertEqual(locator.as_course_locator(), Locator.to_locator_or_location(locator.as_course_locator()))
        self.assertEqual(location, Locator.to_locator_or_location(location.url()))
        self.assertEqual(locator, Locator.to_locator_or_location(locator.url()))
        self.assertEqual(locator, Locator.to_locator_or_location(locator.__dict__))

        asset_location = Location(['c4x', 'mit', 'eecs.6002x', 'asset', 'selfie.jpeg'])
        self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location))
        self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location.url()))

        def_location_url = "defx://version/" + '{:024x}'.format(random.randrange(16 ** 24))
        self.assertEqual(DefinitionLocator(def_location_url), Locator.to_locator_or_location(def_location_url))

        with self.assertRaises(ValueError):
            Locator.to_locator_or_location(22)
        with self.assertRaises(ValueError):
            Locator.to_locator_or_location("hello.world.not.a.url")
        self.assertIsNone(Locator.parse_url("unknown://foo.bar/baz"))
    def test_update_children(self):
        """
        test updating an item's children ensuring the definition doesn't version but the course does if it should
        """
        locator = BlockUsageLocator(course_id="GreekHero", usage_id="chapter3", branch='draft')
        block = modulestore().get_item(locator)
        pre_def_id = block.definition_locator.definition_id
        pre_version_guid = block.location.version_guid

        # reorder children
        self.assertGreater(len(block.children), 0, "meaningless test")
        moved_child = block.children.pop()
        block.save()  # decache model changes
        updated_problem = modulestore().update_item(block, 'childchanger')
        # check that course version changed and course's previous is the other one
        self.assertEqual(updated_problem.definition_locator.definition_id, pre_def_id)
        self.assertNotEqual(updated_problem.location.version_guid, pre_version_guid)
        self.assertEqual(updated_problem.children, block.children)
        self.assertNotIn(moved_child, updated_problem.children)
        locator.usage_id = "chapter1"
        other_block = modulestore().get_item(locator)
        other_block.children.append(moved_child)
        other_block.save()  # decache model changes
        other_updated = modulestore().update_item(other_block, 'childchanger')
        self.assertIn(moved_child, other_updated.children)
    def test_post_course_update(self):
        """
        Test that a user can successfully post on course updates and handouts of a course
        whose location in not in loc_mapper
        """
        # create a course via the view handler
        course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
        course_locator = loc_mapper().translate_location(
            course_location.course_id, course_location, False, True
        )
        self.client.ajax_post(
            course_locator.url_reverse('course'),
            {
                'org': course_location.org,
                'number': course_location.course,
                'display_name': 'test course',
                'run': course_location.name,
            }
        )

        branch = u'draft'
        version = None
        block = u'updates'
        updates_locator = BlockUsageLocator(
            package_id=course_location.course_id.replace('/', '.'), branch=branch, version_guid=version, block_id=block
        )

        content = u"Sample update"
        payload = {'content': content, 'date': 'January 8, 2013'}
        course_update_url = updates_locator.url_reverse('course_info_update')
        resp = self.client.ajax_post(course_update_url, payload)

        # check that response status is 200 not 400
        self.assertEqual(resp.status_code, 200)

        payload = json.loads(resp.content)
        self.assertHTMLEqual(payload['content'], content)

        # now test that calling translate_location returns a locator whose block_id is 'updates'
        updates_location = course_location.replace(category='course_info', name=block)
        updates_locator = loc_mapper().translate_location(course_location.course_id, updates_location)
        self.assertTrue(isinstance(updates_locator, BlockUsageLocator))
        self.assertEqual(updates_locator.block_id, block)

        # check posting on handouts
        block = u'handouts'
        handouts_locator = BlockUsageLocator(
            package_id=updates_locator.package_id, branch=updates_locator.branch, version_guid=version, block_id=block
        )
        course_handouts_url = handouts_locator.url_reverse('xblock')
        content = u"Sample handout"
        payload = {"data": content}
        resp = self.client.ajax_post(course_handouts_url, payload)

        # check that response status is 200 not 500
        self.assertEqual(resp.status_code, 200)

        payload = json.loads(resp.content)
        self.assertHTMLEqual(payload['data'], content)
Example #5
0
 def test_block_constructor(self):
     testurn = 'mit.eecs.6002x;published#HW3'
     expected_id = 'mit.eecs.6002x'
     expected_branch = 'published'
     expected_block_ref = 'HW3'
     testobj = BlockUsageLocator(course_id=testurn)
     self.check_block_locn_fields(testobj, 'test_block constructor',
                                  course_id=expected_id,
                                  branch=expected_branch,
                                  block=expected_block_ref)
     self.assertEqual(str(testobj), testurn)
     self.assertEqual(testobj.url(), 'edx://' + testurn)
Example #6
0
def grading_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None, grader_index=None):
    """
    Course Grading policy configuration
    GET
        html: get the page
        json no grader_index: get the CourseGrading model (graceperiod, cutoffs, and graders)
        json w/ grader_index: get the specific grader
    PUT
        json no grader_index: update the Course through the CourseGrading model
        json w/ grader_index: create or update the specific grader (create if index out of range)
    """
    locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    if not has_access(request.user, locator):
        raise PermissionDenied()

    if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
        course_old_location = loc_mapper().translate_locator_to_location(locator)
        course_module = modulestore().get_item(course_old_location)
        course_details = CourseGradingModel.fetch(locator)

        return render_to_response('settings_graders.html', {
            'context_course': course_module,
            'course_locator': locator,
            'course_details': json.dumps(course_details, cls=CourseSettingsEncoder),
            'grading_url': locator.url_reverse('/settings/grading/'),
        })
    elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
        if request.method == 'GET':
            if grader_index is None:
                return JsonResponse(
                    CourseGradingModel.fetch(locator),
                    # encoder serializes dates, old locations, and instances
                    encoder=CourseSettingsEncoder
                )
            else:
                return JsonResponse(CourseGradingModel.fetch_grader(locator, grader_index))
        elif request.method in ('POST', 'PUT'):  # post or put, doesn't matter.
            # None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader
            if grader_index is None:
                return JsonResponse(
                    CourseGradingModel.update_from_json(locator, request.json),
                    encoder=CourseSettingsEncoder
                )
            else:
                return JsonResponse(
                    CourseGradingModel.update_grader_from_json(locator, request.json)
                )
        elif request.method == "DELETE" and grader_index is not None:
            CourseGradingModel.delete_grader(locator, grader_index)
            return JsonResponse()
 def test_get_parents(self):
     '''
     get_parent_locations(locator, [usage_id], [branch]): [BlockUsageLocator]
     '''
     locator = BlockUsageLocator(course_id="GreekHero", branch='draft', usage_id='chapter1')
     parents = modulestore().get_parent_locations(locator)
     self.assertEqual(len(parents), 1)
     self.assertEqual(parents[0].usage_id, 'head12345')
     self.assertEqual(parents[0].course_id, "GreekHero")
     locator.usage_id = 'chapter2'
     parents = modulestore().get_parent_locations(locator)
     self.assertEqual(len(parents), 1)
     self.assertEqual(parents[0].usage_id, 'head12345')
     locator.usage_id = 'nosuchblock'
     parents = modulestore().get_parent_locations(locator)
     self.assertEqual(len(parents), 0)
Example #8
0
 def test_block_constructor(self):
     testurn = "mit.eecs.6002x" + BRANCH_PREFIX + "published" + BLOCK_PREFIX + "HW3"
     expected_id = "mit.eecs.6002x"
     expected_branch = "published"
     expected_block_ref = "HW3"
     testobj = BlockUsageLocator(course_id=testurn)
     self.check_block_locn_fields(
         testobj, "test_block constructor", course_id=expected_id, branch=expected_branch, block=expected_block_ref
     )
     self.assertEqual(str(testobj), testurn)
     self.assertEqual(testobj.url(), "edx://" + testurn)
     agnostic = testobj.version_agnostic()
     self.assertIsNone(agnostic.version_guid)
     self.check_block_locn_fields(
         agnostic, "test_block constructor", course_id=expected_id, branch=expected_branch, block=expected_block_ref
     )
Example #9
0
 def test_relative(self):
     """
     Test making a relative usage locator.
     """
     package_id = 'mit.eecs-1'
     branch = 'foo'
     baseobj = CourseLocator(package_id=package_id, branch=branch)
     block_id = 'problem:with-colon~2'
     testobj = BlockUsageLocator.make_relative(baseobj, block_id)
     self.check_block_locn_fields(
         testobj, 'Cannot make relative to course', package_id=package_id, branch=branch, block=block_id
     )
     block_id = 'completely_different'
     testobj = BlockUsageLocator.make_relative(testobj, block_id)
     self.check_block_locn_fields(
         testobj, 'Cannot make relative to block usage', package_id=package_id, branch=branch, block=block_id
     )
Example #10
0
 def test_relative(self):
     """
     Test making a relative usage locator.
     """
     org = 'mit.eecs'
     offering = '1'
     branch = 'foo'
     baseobj = CourseLocator(org=org, offering=offering, branch=branch)
     block_id = 'problem:with-colon~2'
     testobj = BlockUsageLocator.make_relative(baseobj, 'problem', block_id)
     self.check_block_locn_fields(
         testobj, org=org, offering=offering, branch=branch, block=block_id
     )
     block_id = 'completely_different'
     testobj = BlockUsageLocator.make_relative(testobj, 'problem', block_id)
     self.check_block_locn_fields(
         testobj, org=org, offering=offering, branch=branch, block=block_id
     )
Example #11
0
 def test_block_constructor(self):
     testurn = 'mit.eecs.6002x/' + BRANCH_PREFIX + 'published/' + BLOCK_PREFIX + 'HW3'
     expected_id = 'mit.eecs.6002x'
     expected_branch = 'published'
     expected_block_ref = 'HW3'
     testobj = BlockUsageLocator(package_id=testurn)
     self.check_block_locn_fields(testobj, 'test_block constructor',
                                  package_id=expected_id,
                                  branch=expected_branch,
                                  block=expected_block_ref)
     self.assertEqual(str(testobj), testurn)
     self.assertEqual(testobj.url(), 'edx://' + testurn)
     agnostic = testobj.version_agnostic()
     self.assertIsNone(agnostic.version_guid)
     self.check_block_locn_fields(agnostic, 'test_block constructor',
                                  package_id=expected_id,
                                  branch=expected_branch,
                                  block=expected_block_ref)
Example #12
0
    def test_block_generations(self):
        """
        Test get_block_generations
        """
        test_course = persistent_factories.PersistentCourseFactory.create(
            offering='history.hist101', org='edu.harvard',
            display_name='history test course',
            user_id='testbot'
        )
        chapter = persistent_factories.ItemFactory.create(display_name='chapter 1',
            parent_location=test_course.location, user_id='testbot')
        sub = persistent_factories.ItemFactory.create(display_name='subsection 1',
            parent_location=chapter.location, user_id='testbot', category='vertical')
        first_problem = persistent_factories.ItemFactory.create(
            display_name='problem 1', parent_location=sub.location, user_id='testbot', category='problem',
            data="<problem></problem>"
        )
        first_problem.max_attempts = 3
        first_problem.save()  # decache the above into the kvs
        updated_problem = modulestore('split').update_item(first_problem, '**replace_user**')
        self.assertIsNotNone(updated_problem.previous_version)
        self.assertEqual(updated_problem.previous_version, first_problem.update_version)
        self.assertNotEqual(updated_problem.update_version, first_problem.update_version)
        updated_loc = modulestore('split').delete_item(updated_problem.location, 'testbot', delete_children=True)

        second_problem = persistent_factories.ItemFactory.create(
            display_name='problem 2',
            parent_location=BlockUsageLocator.make_relative(
                updated_loc, block_type='problem', block_id=sub.location.block_id
            ),
            user_id='testbot', category='problem',
            data="<problem></problem>"
        )

        # course root only updated 2x
        version_history = modulestore('split').get_block_generations(test_course.location)
        self.assertEqual(version_history.locator.version_guid, test_course.location.version_guid)
        self.assertEqual(len(version_history.children), 1)
        self.assertEqual(version_history.children[0].children, [])
        self.assertEqual(version_history.children[0].locator.version_guid, chapter.location.version_guid)

        # sub changed on add, add problem, delete problem, add problem in strict linear seq
        version_history = modulestore('split').get_block_generations(sub.location)
        self.assertEqual(len(version_history.children), 1)
        self.assertEqual(len(version_history.children[0].children), 1)
        self.assertEqual(len(version_history.children[0].children[0].children), 1)
        self.assertEqual(len(version_history.children[0].children[0].children[0].children), 0)

        # first and second problem may show as same usage_id; so, need to ensure their histories are right
        version_history = modulestore('split').get_block_generations(updated_problem.location)
        self.assertEqual(version_history.locator.version_guid, first_problem.location.version_guid)
        self.assertEqual(len(version_history.children), 1)  # updated max_attempts
        self.assertEqual(len(version_history.children[0].children), 0)

        version_history = modulestore('split').get_block_generations(second_problem.location)
        self.assertNotEqual(version_history.locator.version_guid, first_problem.location.version_guid)
Example #13
0
 def test_block_constructor_url_version_prefix(self):
     test_id_loc = "519665f6223ebd6980884f2b"
     testobj = BlockUsageLocator(url="edx://mit.eecs.6002x" + VERSION_PREFIX + test_id_loc + BLOCK_PREFIX + "lab2")
     self.check_block_locn_fields(
         testobj,
         "error parsing URL with version and block",
         course_id="mit.eecs.6002x",
         block="lab2",
         version_guid=ObjectId(test_id_loc),
     )
     agnostic = testobj.version_agnostic()
     self.check_block_locn_fields(
         agnostic,
         "error parsing URL with version and block",
         block="lab2",
         course_id=None,
         version_guid=ObjectId(test_id_loc),
     )
     self.assertIsNone(agnostic.course_id)
Example #14
0
 def test_block_constructor_url_version_prefix(self):
     test_id_loc = '519665f6223ebd6980884f2b'
     testobj = BlockUsageLocator(
         url='edx://mit.eecs.6002x/{}{}/{}lab2'.format(VERSION_PREFIX, test_id_loc, BLOCK_PREFIX)
     )
     self.check_block_locn_fields(
         testobj, 'error parsing URL with version and block',
         package_id='mit.eecs.6002x',
         block='lab2',
         version_guid=ObjectId(test_id_loc)
     )
     agnostic = testobj.version_agnostic()
     self.check_block_locn_fields(
         agnostic, 'error parsing URL with version and block',
         block='lab2',
         package_id=None,
         version_guid=ObjectId(test_id_loc)
     )
     self.assertIsNone(agnostic.package_id)
    def test_ensure_fully_specd(self):
        '''
        Test constructor and property accessors.
        '''
        raise SkipTest()
        self.assertRaises(InsufficientSpecificationError,
                          BlockUsageLocator.ensure_fully_specified, BlockUsageLocator())

        # url inits
        testurn = 'edx://org/course/category/name'
        self.assertRaises(InvalidLocationError,
                          BlockUsageLocator.ensure_fully_specified, testurn)
        testurn = 'unknown/versionid/blockid'
        self.assertRaises(InvalidLocationError,
                          BlockUsageLocator.ensure_fully_specified, testurn)

        testurn = 'cvx/versionid'
        self.assertRaises(InsufficientSpecificationError,
                          BlockUsageLocator.ensure_fully_specified, testurn)

        testurn = 'cvx/versionid/'
        self.assertRaises(InsufficientSpecificationError,
                          BlockUsageLocator.ensure_fully_specified, testurn)

        testurn = 'cvx/versionid/blockid'
        self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn),
                              BlockUsageLocator, testurn)

        testurn = 'cvx/versionid/blockid/extraneousstuff?including=args'
        self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn),
                              BlockUsageLocator, testurn)

        testurn = 'cvx://versionid/blockid'
        self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn),
                              BlockUsageLocator, testurn)

        testurn = 'crx/courseid/blockid'
        self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn),
                              BlockUsageLocator, testurn)

        testurn = 'crx/courseid@revision/blockid'
        self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn),
                              BlockUsageLocator, testurn)
Example #16
0
def settings_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
    """
    Course settings for dates and about pages
    GET
        html: get the page
        json: get the CourseDetails model
    PUT
        json: update the Course and About xblocks through the CourseDetails model
    """
    locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    if not has_access(request.user, locator):
        raise PermissionDenied()

    if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
        course_old_location = loc_mapper().translate_locator_to_location(locator)
        course_module = modulestore().get_item(course_old_location)

        upload_asset_url = locator.url_reverse('assets/')

        return render_to_response('settings.html', {
            'context_course': course_module,
            'course_locator': locator,
            'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_old_location),
            'course_image_url': utils.course_image_url(course_module),
            'details_url': locator.url_reverse('/settings/details/'),
            'about_page_editable': not settings.FEATURES.get(
                'ENABLE_MKTG_SITE', False
            ),
            'upload_asset_url': upload_asset_url
        })
    elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
        if request.method == 'GET':
            return JsonResponse(
                CourseDetails.fetch(locator),
                # encoder serializes dates, old locations, and instances
                encoder=CourseSettingsEncoder
            )
        else:  # post or put, doesn't matter.
            return JsonResponse(
                CourseDetails.update_from_json(locator, request.json),
                encoder=CourseSettingsEncoder
            )
Example #17
0
def advanced_settings_handler(request, course_id=None, branch=None, version_guid=None, block=None, tag=None):
    """
    Course settings configuration
    GET
        html: get the page
        json: get the model
    PUT, POST
        json: update the Course's settings. The payload is a json rep of the
            metadata dicts. The dict can include a "unsetKeys" entry which is a list
            of keys whose values to unset: i.e., revert to default
    """
    locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    if not has_access(request.user, locator):
        raise PermissionDenied()

    course_old_location = loc_mapper().translate_locator_to_location(locator)
    course_module = modulestore().get_item(course_old_location)

    if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':

        return render_to_response('settings_advanced.html', {
            'context_course': course_module,
            'advanced_dict': json.dumps(CourseMetadata.fetch(course_module)),
            'advanced_settings_url': locator.url_reverse('settings/advanced')
        })
    elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
        if request.method == 'GET':
            return JsonResponse(CourseMetadata.fetch(course_module))
        else:
            # Whether or not to filter the tabs key out of the settings metadata
            filter_tabs = _config_course_advanced_components(request, course_module)
            try:
                return JsonResponse(CourseMetadata.update_from_json(
                    course_module,
                    request.json,
                    filter_tabs=filter_tabs
                ))
            except (TypeError, ValueError) as err:
                return HttpResponseBadRequest(
                    "Incorrect setting format. {}".format(err),
                    content_type="text/plain"
                )
    def test_ensure_fully_via_keyword(self):
        # arg list inits
        raise SkipTest()
        testobj = BlockUsageLocator(version_guid='versionid')
        self.assertRaises(InsufficientSpecificationError,
                          BlockUsageLocator.ensure_fully_specified, testobj)

        testurn = 'crx/courseid@revision/blockid'
        testobj = BlockUsageLocator(version_guid='versionid', usage_id='myblock')
        self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn),
                              BlockUsageLocator, testurn)

        testobj = BlockUsageLocator(course_id='courseid')
        self.assertRaises(InsufficientSpecificationError,
                          BlockUsageLocator.ensure_fully_specified, testobj)

        testobj = BlockUsageLocator(course_id='courseid', revision='rev')
        self.assertRaises(InsufficientSpecificationError,
                          BlockUsageLocator.ensure_fully_specified, testobj)

        testobj = BlockUsageLocator(course_id='courseid', revision='rev',
                                    usage_id='this_block')
        self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn),
                              BlockUsageLocator, testurn)
    def test_has_item(self):
        '''
        has_item(BlockUsageLocator)
        '''
        course_id = 'GreekHero'
        # positive tests of various forms
        locator = BlockUsageLocator(version_guid=self.GUID_D1, usage_id='head12345')
        self.assertTrue(modulestore().has_item(course_id, locator),
                        "couldn't find in %s" % self.GUID_D1)

        locator = BlockUsageLocator(course_id='GreekHero', usage_id='head12345', branch='draft')
        self.assertTrue(
            modulestore().has_item(locator.course_id, locator),
            "couldn't find in 12345"
        )
        self.assertTrue(
            modulestore().has_item(locator.course_id, BlockUsageLocator(
                course_id=locator.course_id,
                branch='draft',
                usage_id=locator.usage_id
            )),
            "couldn't find in draft 12345"
        )
        self.assertFalse(
            modulestore().has_item(locator.course_id, BlockUsageLocator(
                course_id=locator.course_id,
                branch='published',
                usage_id=locator.usage_id)),
            "found in published 12345"
        )
        locator.branch = 'draft'
        self.assertTrue(
            modulestore().has_item(locator.course_id, locator),
            "not found in draft 12345"
        )

        # not a course obj
        locator = BlockUsageLocator(course_id='GreekHero', usage_id='chapter1', branch='draft')
        self.assertTrue(
            modulestore().has_item(locator.course_id, locator),
            "couldn't find chapter1"
        )

        # in published course
        locator = BlockUsageLocator(course_id="wonderful", usage_id="head23456", branch='draft')
        self.assertTrue(
            modulestore().has_item(
                locator.course_id,
                BlockUsageLocator(course_id=locator.course_id, usage_id=locator.usage_id, branch='published')
            ), "couldn't find in 23456"
        )
        locator.branch = 'published'
        self.assertTrue(modulestore().has_item(course_id, locator), "couldn't find in 23456")
Example #20
0
def tabs_handler(request,
                 tag=None,
                 package_id=None,
                 branch=None,
                 version_guid=None,
                 block=None):
    """
    The restful handler for static tabs.

    GET
        html: return page for editing static tabs
        json: not supported
    PUT or POST
        json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs".
        The value for "tabs" is an array of tab locators, indicating the desired order of the tabs.

    Creating a tab, deleting a tab, or changing its contents is not supported through this method.
    Instead use the general xblock URL (see item.xblock_handler).
    """
    locator = BlockUsageLocator(package_id=package_id,
                                branch=branch,
                                version_guid=version_guid,
                                block_id=block)
    if not has_course_access(request.user, locator):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(locator)
    store = get_modulestore(old_location)
    course_item = store.get_item(old_location)

    if 'application/json' in request.META.get('HTTP_ACCEPT',
                                              'application/json'):
        if request.method == 'GET':
            raise NotImplementedError('coming soon')
        else:
            if 'tabs' in request.json:

                def get_location_for_tab(tab):
                    """  Returns the location (old-style) for a tab. """
                    return loc_mapper().translate_locator_to_location(
                        BlockUsageLocator(tab))

                tabs = request.json['tabs']

                # get list of existing static tabs in course
                # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
                # that we know about) otherwise we will inadvertently drop some!
                existing_static_tabs = [
                    t for t in course_item.tabs if t['type'] == 'static_tab'
                ]
                if len(existing_static_tabs) != len(tabs):
                    return JsonResponse(
                        {
                            "error":
                            "number of tabs must be {}".format(
                                len(existing_static_tabs))
                        },
                        status=400)

                # load all reference tabs, return BadRequest if we can't find any of them
                tab_items = []
                for tab in tabs:
                    item = modulestore('direct').get_item(
                        get_location_for_tab(tab))
                    if item is None:
                        return JsonResponse(
                            {
                                "error":
                                "no tab for found location {}".format(tab)
                            },
                            status=400)

                    tab_items.append(item)

                # now just go through the existing course_tabs and re-order the static tabs
                reordered_tabs = []
                static_tab_idx = 0
                for tab in course_item.tabs:
                    if tab['type'] == 'static_tab':
                        reordered_tabs.append({
                            'type':
                            'static_tab',
                            'name':
                            tab_items[static_tab_idx].display_name,
                            'url_slug':
                            tab_items[static_tab_idx].location.name,
                        })
                        static_tab_idx += 1
                    else:
                        reordered_tabs.append(tab)

                # OK, re-assemble the static tabs in the new order
                course_item.tabs = reordered_tabs
                modulestore('direct').update_item(course_item, request.user.id)
                return JsonResponse()
            else:
                raise NotImplementedError(
                    'Creating or changing tab content is not supported.')
    elif request.method == 'GET':  # assume html
        # see tabs have been uninitialized (e.g. supporting courses created before tab support in studio)
        if course_item.tabs is None or len(course_item.tabs) == 0:
            initialize_course_tabs(course_item, request.user)

        # first get all static tabs from the tabs list
        # we do this because this is also the order in which items are displayed in the LMS
        static_tabs_refs = [
            t for t in course_item.tabs if t['type'] == 'static_tab'
        ]

        static_tabs = []
        for static_tab_ref in static_tabs_refs:
            static_tab_loc = old_location.replace(
                category='static_tab', name=static_tab_ref['url_slug'])
            static_tabs.append(modulestore('direct').get_item(static_tab_loc))

        components = [
            loc_mapper().translate_location(course_item.location.course_id,
                                            static_tab.location, False, True)
            for static_tab in static_tabs
        ]

        return render_to_response(
            'edit-tabs.html', {
                'context_course': course_item,
                'components': components,
                'course_locator': locator
            })
    else:
        return HttpResponseNotFound()
Example #21
0
def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for xblock requests.

    DELETE
        json: delete this xblock instance from the course. Supports query parameters "recurse" to delete
        all children and "all_versions" to delete from all (mongo) versions.
    GET
        json: returns representation of the xblock (locator id, data, and metadata).
              if ?fields=graderType, it returns the graderType for the unit instead of the above.
        html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
    PUT or POST
        json: if xblock locator is specified, update the xblock instance. The json payload can contain
              these fields, all optional:
                :data: the new value for the data.
                :children: the locator ids of children for this xblock.
                :metadata: new values for the metadata fields. Any whose values are None will be deleted not set
                       to None! Absent ones will be left alone.
                :nullout: which metadata fields to set to None
                :graderType: change how this unit is graded
                :publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
              The JSON representation on the updated xblock (minus children) is returned.

              if xblock locator is not specified, create a new xblock instance, either by duplicating
              an existing xblock, or creating an entirely new one. The json playload can contain
              these fields:
                :parent_locator: parent for new xblock, required for both duplicate and create new instance
                :duplicate_source_locator: if present, use this as the source for creating a duplicate copy
                :category: type of xblock, required if duplicate_source_locator is not present.
                :display_name: name for new xblock, optional
                :boilerplate: template name for populating fields, optional and only used
                     if duplicate_source_locator is not present
              The locator (and old-style id) for the created xblock (minus children) is returned.
    """
    if package_id is not None:
        locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
        if not has_course_access(request.user, locator):
            raise PermissionDenied()
        old_location = loc_mapper().translate_locator_to_location(locator)

        if request.method == 'GET':
            accept_header = request.META.get('HTTP_ACCEPT', 'application/json')

            if 'application/json' in accept_header:
                fields = request.REQUEST.get('fields', '').split(',')
                if 'graderType' in fields:
                    # right now can't combine output of this w/ output of _get_module_info, but worthy goal
                    return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
                # TODO: pass fields to _get_module_info and only return those
                rsp = _get_module_info(locator)
                return JsonResponse(rsp)
            else:
                return HttpResponse(status=406)

        elif request.method == 'DELETE':
            delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
            delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))

            return _delete_item_at_location(old_location, delete_children, delete_all_versions, request.user)
        else:  # Since we have a package_id, we are updating an existing xblock.
            return _save_item(
                request,
                locator,
                old_location,
                data=request.json.get('data'),
                children=request.json.get('children'),
                metadata=request.json.get('metadata'),
                nullout=request.json.get('nullout'),
                grader_type=request.json.get('graderType'),
                publish=request.json.get('publish'),
            )
    elif request.method in ('PUT', 'POST'):
        if 'duplicate_source_locator' in request.json:
            parent_locator = BlockUsageLocator(request.json['parent_locator'])
            duplicate_source_locator = BlockUsageLocator(request.json['duplicate_source_locator'])

            # _duplicate_item is dealing with locations to facilitate the recursive call for
            # duplicating children.
            parent_location = loc_mapper().translate_locator_to_location(parent_locator)
            duplicate_source_location = loc_mapper().translate_locator_to_location(duplicate_source_locator)
            dest_location = _duplicate_item(
                parent_location,
                duplicate_source_location,
                request.json.get('display_name'),
                request.user,
            )
            course_location = loc_mapper().translate_locator_to_location(BlockUsageLocator(parent_locator), get_course=True)
            dest_locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
            return JsonResponse({"locator": unicode(dest_locator)})
        else:
            return _create_item(request)
    else:
        return HttpResponseBadRequest(
            "Only instance creation is supported without a package_id.",
            content_type="text/plain"
        )
    def test_translate_locator(self):
        """
        tests translate_locator_to_location(BlockUsageLocator)
        """
        # lookup for non-existent course
        org = 'foo_org'
        course = 'bar_course'
        run = 'baz_run'
        new_style_org = '{}.geek_dept'.format(org)
        new_style_offering = '{}.{}'.format(course, run)
        prob_course_key = CourseLocator(
            org=new_style_org, offering=new_style_offering,
            branch='published',
        )
        prob_locator = BlockUsageLocator(
            prob_course_key,
            block_type='problem',
            block_id='problem2',
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertIsNone(prob_location, 'found entry in empty map table')

        loc_mapper().create_map_entry(
            SlashSeparatedCourseKey(org, course, run),
            new_style_org, new_style_offering,
            block_map={
                'abc123': {'problem': 'problem2'},
                '48f23a10395384929234': {'chapter': 'chapter48f'},
                'baz_run': {'course': 'root'},
            }
        )
        # only one course matches
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        # default branch
        self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None))
        # test get_course keyword
        prob_location = loc_mapper().translate_locator_to_location(prob_locator, get_course=True)
        self.assertEqual(prob_location, SlashSeparatedCourseKey(org, course, run))
        # explicit branch
        prob_locator = prob_locator.for_branch('draft')
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        # Even though the problem was set as draft, we always return revision=None to work
        # with old mongo/draft modulestores.
        self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None))
        prob_locator = BlockUsageLocator(
            prob_course_key.for_branch('production'),
            block_type='problem', block_id='problem2'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None))
        # same for chapter except chapter cannot be draft in old system
        chap_locator = BlockUsageLocator(
            prob_course_key.for_branch('production'),
            block_type='chapter', block_id='chapter48f',
        )
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234'))
        # explicit branch
        chap_locator = chap_locator.for_branch('draft')
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234'))
        chap_locator = BlockUsageLocator(
            prob_course_key.for_branch('production'), block_type='chapter', block_id='chapter48f'
        )
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234'))

        # look for non-existent problem
        prob_locator2 = BlockUsageLocator(
            prob_course_key.for_branch('draft'),
            block_type='problem', block_id='problem3'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator2)
        self.assertIsNone(prob_location, 'Found non-existent problem')

        # add a distractor course
        delta_run = 'delta_run'
        new_style_offering = '{}.{}'.format(course, delta_run)
        loc_mapper().create_map_entry(
            SlashSeparatedCourseKey(org, course, delta_run),
            new_style_org, new_style_offering,
            block_map={'abc123': {'problem': 'problem3'}}
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None))
 def test_delete_block(self):
     """
     test delete_block_location_translator(location, old_course_id=None)
     """
     org = 'foo_org'
     course = 'bar_course'
     new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'baz_run'),
                                   new_style_course_id,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem2'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48f'
                                       },
                                       '1': {
                                           'chapter': 'chapter1',
                                           'problem': 'problem1'
                                       },
                                   })
     new_style_course_id2 = '{}.geek_dept.{}.delta_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'delta_run'),
                                   new_style_course_id2,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem3'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48b'
                                       },
                                       '1': {
                                           'chapter': 'chapter2',
                                           'problem': 'problem2'
                                       },
                                   })
     location = Location('i4x', org, course, 'problem', '1')
     # delete from all courses
     loc_mapper().delete_block_location_translator(location)
     self.assertIsNone(loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id,
                           usage_id='problem1')))
     self.assertIsNone(loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id2,
                           usage_id='problem2')))
     # delete from one course
     location = location.replace(name='abc123')
     loc_mapper().delete_block_location_translator(
         location, '{}/{}/{}'.format(org, course, 'baz_run'))
     with self.assertRaises(ItemNotFoundError):
         loc_mapper().translate_location('{}/{}/{}'.format(
             org, course, 'baz_run'),
                                         location,
                                         add_entry_if_missing=False)
     locator = loc_mapper().translate_location('{}/{}/{}'.format(
         org, course, 'delta_run'),
                                               location,
                                               add_entry_if_missing=False)
     self.assertEqual(locator.usage_id, 'problem3')
    def test_translate_locator(self):
        """
        tests translate_locator_to_location(BlockUsageLocator)
        """
        # lookup for non-existent course
        org = 'foo_org'
        course = 'bar_course'
        new_style_package_id = '{}.geek_dept.{}.baz_run'.format(org, course)
        prob_locator = BlockUsageLocator(
            package_id=new_style_package_id,
            block_id='problem2',
            branch='published'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertIsNone(prob_location, 'found entry in empty map table')

        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'baz_run'),
            new_style_package_id,
            block_map={
                'abc123': {'problem': 'problem2'},
                '48f23a10395384929234': {'chapter': 'chapter48f'},
                'baz_run': {'course': 'root'},
            }
        )
        # only one course matches
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        # default branch
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))
        # test get_course keyword
        prob_location = loc_mapper().translate_locator_to_location(prob_locator, get_course=True)
        self.assertEqual(prob_location, Location('i4x', org, course, 'course', 'baz_run', None))
        # explicit branch
        prob_locator = BlockUsageLocator(
            package_id=prob_locator.package_id, branch='draft', block_id=prob_locator.block_id
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        # Even though the problem was set as draft, we always return revision=None to work
        # with old mongo/draft modulestores.
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))
        prob_locator = BlockUsageLocator(
            package_id=new_style_package_id, block_id='problem2', branch='production'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))
        # same for chapter except chapter cannot be draft in old system
        chap_locator = BlockUsageLocator(
            package_id=new_style_package_id,
            block_id='chapter48f',
            branch='production'
        )
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        # explicit branch
        chap_locator.branch = 'draft'
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        chap_locator = BlockUsageLocator(
            package_id=new_style_package_id, block_id='chapter48f', branch='production'
        )
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234'))

        # look for non-existent problem
        prob_locator2 = BlockUsageLocator(
            package_id=new_style_package_id,
            branch='draft',
            block_id='problem3'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator2)
        self.assertIsNone(prob_location, 'Found non-existent problem')

        # add a distractor course
        new_style_package_id = '{}.geek_dept.{}.{}'.format(org, course, 'delta_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'delta_run'),
            new_style_package_id,
            block_map={'abc123': {'problem': 'problem3'}}
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))

        # add a default course pointing to the delta_run
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'problem', '789abc123efg456'),
            new_style_package_id,
            block_map={'abc123': {'problem': 'problem3'}}
        )
        # now query delta (2 entries point to it)
        prob_locator = BlockUsageLocator(
            package_id=new_style_package_id,
            branch='production',
            block_id='problem3'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123'))
Example #25
0
def checklists_handler(request,
                       tag=None,
                       package_id=None,
                       branch=None,
                       version_guid=None,
                       block=None,
                       checklist_index=None):
    """
    The restful handler for checklists.

    GET
        html: return html page for all checklists
        json: return json representing all checklists. checklist_index is not supported for GET at this time.
    POST or PUT
        json: updates the checked state for items within a particular checklist. checklist_index is required.
    """
    location = BlockUsageLocator(package_id=package_id,
                                 branch=branch,
                                 version_guid=version_guid,
                                 block_id=block)
    if not has_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)

    modulestore = get_modulestore(old_location)
    course_module = modulestore.get_item(old_location)

    json_request = 'application/json' in request.META.get(
        'HTTP_ACCEPT', 'application/json')
    if request.method == 'GET':
        # If course was created before checklists were introduced, copy them over
        # from the template.
        if not course_module.checklists:
            course_module.checklists = CourseDescriptor.checklists.default
            course_module.save()
            modulestore.update_metadata(old_location,
                                        own_metadata(course_module))

        expanded_checklists = expand_all_action_urls(course_module)
        if json_request:
            return JsonResponse(expanded_checklists)
        else:
            handler_url = location.url_reverse('checklists/', '')
            return render_to_response(
                'checklists.html',
                {
                    'handler_url': handler_url,
                    # context_course is used by analytics
                    'context_course': course_module,
                    'checklists': expanded_checklists
                })
    elif json_request:
        # Can now assume POST or PUT because GET handled above.
        if checklist_index is not None and 0 <= int(checklist_index) < len(
                course_module.checklists):
            index = int(checklist_index)
            persisted_checklist = course_module.checklists[index]
            modified_checklist = json.loads(request.body)
            # Only thing the user can modify is the "checked" state.
            # We don't want to persist what comes back from the client because it will
            # include the expanded action URLs (which are non-portable).
            for item_index, item in enumerate(modified_checklist.get('items')):
                persisted_checklist['items'][item_index]['is_checked'] = item[
                    'is_checked']
            # seeming noop which triggers kvs to record that the metadata is
            # not default
            course_module.checklists = course_module.checklists
            course_module.save()
            modulestore.update_metadata(old_location,
                                        own_metadata(course_module))
            expanded_checklist = expand_checklist_action_url(
                course_module, persisted_checklist)
            return JsonResponse(expanded_checklist)
        else:
            return HttpResponseBadRequest(
                ("Could not save checklist state because the checklist index "
                 "was out of range or unspecified."),
                content_type="text/plain")
    else:
        return HttpResponseNotFound()
Example #26
0
    def xblock_from_json(self,
                         class_,
                         usage_id,
                         json_data,
                         course_entry_override=None):
        if course_entry_override is None:
            course_entry_override = self.course_entry
        else:
            # most recent retrieval is most likely the right one for next caller (see comment above fn)
            self.course_entry['branch'] = course_entry_override['branch']
            self.course_entry['course_id'] = course_entry_override['course_id']
        # most likely a lazy loader or the id directly
        definition = json_data.get('definition', {})
        definition_id = self.modulestore.definition_locator(definition)

        # If no usage id is provided, generate an in-memory id
        if usage_id is None:
            usage_id = LocalId()

        block_locator = BlockUsageLocator(
            version_guid=course_entry_override['structure']['_id'],
            usage_id=usage_id,
            course_id=course_entry_override.get('course_id'),
            branch=course_entry_override.get('branch'))

        kvs = SplitMongoKVS(
            definition,
            json_data.get('fields', {}),
            json_data.get('_inherited_settings'),
        )
        field_data = DbModel(kvs)

        try:
            module = self.construct_xblock_from_class(
                class_,
                ScopeIds(None, json_data.get('category'), definition_id,
                         block_locator),
                field_data,
            )
        except Exception:
            log.warning("Failed to load descriptor", exc_info=True)
            return ErrorDescriptor.from_json(
                json_data,
                self,
                BlockUsageLocator(
                    version_guid=course_entry_override['structure']['_id'],
                    usage_id=usage_id),
                error_msg=exc_info_to_str(sys.exc_info()))

        edit_info = json_data.get('edit_info', {})
        module.edited_by = edit_info.get('edited_by')
        module.edited_on = edit_info.get('edited_on')
        module.previous_version = edit_info.get('previous_version')
        module.update_version = edit_info.get('update_version')
        module.definition_locator = self.modulestore.definition_locator(
            definition)
        # decache any pending field settings
        module.save()

        # If this is an in-memory block, store it in this system
        if isinstance(block_locator.usage_id, LocalId):
            self.local_modules[block_locator] = module

        return module
Example #27
0
    def translate_location(self,
                           old_style_course_id,
                           location,
                           published=True,
                           add_entry_if_missing=True):
        """
        Translate the given module location to a Locator. If the mapping has the run id in it, then you
        should provide old_style_course_id with that run id in it to disambiguate the mapping if there exists more
        than one entry in the mapping table for the org.course.

        The rationale for auto adding entries was that there should be a reasonable default translation
        if the code just trips into this w/o creating translations. The downfall is that ambiguous course
        locations may generate conflicting block_ids.

        Will raise ItemNotFoundError if there's no mapping and add_entry_if_missing is False.

        :param old_style_course_id: the course_id used in old mongo not the new one (optional, will use location)
        :param location:  a Location pointing to a module
        :param published: a boolean to indicate whether the caller wants the draft or published branch.
        :param add_entry_if_missing: a boolean as to whether to raise ItemNotFoundError or to create an entry if
        the course
        or block is not found in the map.

        NOTE: unlike old mongo, draft branches contain the whole course; so, it applies to all category
        of locations including course.
        """
        location_id = self._interpret_location_course_id(
            old_style_course_id, location)

        maps = self.location_map.find(location_id).sort(
            '_id.name', pymongo.ASCENDING)
        if maps.count() == 0:
            if add_entry_if_missing:
                # create a new map
                course_location = location.replace(
                    category='course', name=location_id['_id.name'])
                self.create_map_entry(course_location)
                entry = self.location_map.find_one(location_id)
            else:
                raise ItemNotFoundError()
        elif maps.count() > 1:
            # if more than one, prefer the one w/o a name if that exists. Otherwise, choose the first (alphabetically)
            entry = maps[0]
        else:
            entry = maps[0]

        if published:
            branch = entry['prod_branch']
        else:
            branch = entry['draft_branch']

        usage_id = entry['block_map'].get(location.name)
        if usage_id is None:
            if add_entry_if_missing:
                usage_id = self._add_to_block_map(location, location_id,
                                                  entry['block_map'])
            else:
                raise ItemNotFoundError()
        elif isinstance(usage_id, dict):
            # name is not unique, look through for the right category
            if location.category in usage_id:
                usage_id = usage_id[location.category]
            elif add_entry_if_missing:
                usage_id = self._add_to_block_map(location, location_id,
                                                  entry['block_map'])
            else:
                raise ItemNotFoundError()
        else:
            raise InvalidLocationError()

        return BlockUsageLocator(course_id=entry['course_id'],
                                 branch=branch,
                                 usage_id=usage_id)
Example #28
0
    def translate_location(self, old_style_course_id, location, published=True, add_entry_if_missing=True):
        """
        Translate the given module location to a Locator. If the mapping has the run id in it, then you
        should provide old_style_course_id with that run id in it to disambiguate the mapping if there exists more
        than one entry in the mapping table for the org.course.

        The rationale for auto adding entries was that there should be a reasonable default translation
        if the code just trips into this w/o creating translations. The downfall is that ambiguous course
        locations may generate conflicting block_ids.

        Will raise ItemNotFoundError if there's no mapping and add_entry_if_missing is False.

        :param old_style_course_id: the course_id used in old mongo not the new one (optional, will use location)
        :param location:  a Location pointing to a module
        :param published: a boolean to indicate whether the caller wants the draft or published branch.
        :param add_entry_if_missing: a boolean as to whether to raise ItemNotFoundError or to create an entry if
        the course
        or block is not found in the map.

        NOTE: unlike old mongo, draft branches contain the whole course; so, it applies to all category
        of locations including course.
        """
        location_id = self._interpret_location_course_id(old_style_course_id, location)
        if old_style_course_id is None:
            old_style_course_id = self._generate_location_course_id(location_id)

        cached_value = self._get_locator_from_cache(old_style_course_id, location, published)
        if cached_value:
            return cached_value

        maps = self.location_map.find(location_id)
        maps = list(maps)
        if len(maps) == 0:
            if add_entry_if_missing:
                # create a new map
                course_location = location.replace(category='course', name=location_id['_id']['name'])
                self.create_map_entry(course_location)
                entry = self.location_map.find_one(location_id)
            else:
                raise ItemNotFoundError()
        elif len(maps) == 1:
            entry = maps[0]
        else:
            # find entry w/o name, if any; otherwise, pick arbitrary
            entry = maps[0]
            for item in maps:
                if 'name' not in item['_id']:
                    entry = item
                    break

        block_id = entry['block_map'].get(self.encode_key_for_mongo(location.name))
        if block_id is None:
            if add_entry_if_missing:
                block_id = self._add_to_block_map(location, location_id, entry['block_map'])
            else:
                raise ItemNotFoundError(location)
        elif isinstance(block_id, dict):
            # jump_to_id uses a None category.
            if location.category is None:
                if len(block_id) == 1:
                    # unique match (most common case)
                    block_id = block_id.values()[0]
                else:
                    raise InvalidLocationError()
            elif location.category in block_id:
                block_id = block_id[location.category]
            elif add_entry_if_missing:
                block_id = self._add_to_block_map(location, location_id, entry['block_map'])
            else:
                raise ItemNotFoundError()
        else:
            raise InvalidLocationError()

        published_usage = BlockUsageLocator(
            package_id=entry['course_id'], branch=entry['prod_branch'], block_id=block_id)
        draft_usage = BlockUsageLocator(
            package_id=entry['course_id'], branch=entry['draft_branch'], block_id=block_id)
        if published:
            result = published_usage
        else:
            result = draft_usage

        self._cache_location_map_entry(old_style_course_id, location, published_usage, draft_usage)
        return result
Example #29
0
def xblock_handler(request,
                   tag=None,
                   course_id=None,
                   branch=None,
                   version_guid=None,
                   block=None):
    """
    The restful handler for xblock requests.

    DELETE
        json: delete this xblock instance from the course. Supports query parameters "recurse" to delete
        all children and "all_versions" to delete from all (mongo) versions.
    GET
        json: returns representation of the xblock (locator id, data, and metadata).
              if ?fields=graderType, it returns the graderType for the unit instead of the above.
        html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
    PUT or POST
        json: if xblock locator is specified, update the xblock instance. The json payload can contain
              these fields, all optional:
                :data: the new value for the data.
                :children: the locator ids of children for this xblock.
                :metadata: new values for the metadata fields. Any whose values are None will be deleted not set
                       to None! Absent ones will be left alone.
                :nullout: which metadata fields to set to None
                :graderType: change how this unit is graded
                :publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
              The JSON representation on the updated xblock (minus children) is returned.

              if xblock locator is not specified, create a new xblock instance. The json playload can contain
              these fields:
                :parent_locator: parent for new xblock, required
                :category: type of xblock, required
                :display_name: name for new xblock, optional
                :boilerplate: template name for populating fields, optional
              The locator (and old-style id) for the created xblock (minus children) is returned.
    """
    if course_id is not None:
        locator = BlockUsageLocator(course_id=course_id,
                                    branch=branch,
                                    version_guid=version_guid,
                                    usage_id=block)
        if not has_access(request.user, locator):
            raise PermissionDenied()
        old_location = loc_mapper().translate_locator_to_location(locator)

        if request.method == 'GET':
            if 'application/json' in request.META.get('HTTP_ACCEPT',
                                                      'application/json'):
                fields = request.REQUEST.get('fields', '').split(',')
                if 'graderType' in fields:
                    # right now can't combine output of this w/ output of _get_module_info, but worthy goal
                    return JsonResponse(
                        CourseGradingModel.get_section_grader_type(locator))
                # TODO: pass fields to _get_module_info and only return those
                rsp = _get_module_info(locator)
                return JsonResponse(rsp)
            else:
                component = modulestore().get_item(old_location)
                # Wrap the generated fragment in the xmodule_editor div so that the javascript
                # can bind to it correctly
                component.runtime.wrappers.append(
                    partial(wrap_xblock, handler_prefix))

                try:
                    content = component.render('studio_view').content
                # catch exceptions indiscriminately, since after this point they escape the
                # dungeon and surface as uneditable, unsaveable, and undeletable
                # component-goblins.
                except Exception as exc:  # pylint: disable=W0703
                    log.debug("Unable to render studio_view for %r",
                              component,
                              exc_info=True)
                    content = render_to_string('html_error.html',
                                               {'message': str(exc)})

                return render_to_response(
                    'component.html', {
                        'preview': get_preview_html(request, component),
                        'editor': content
                    })
        elif request.method == 'DELETE':
            delete_children = str_to_bool(
                request.REQUEST.get('recurse', 'False'))
            delete_all_versions = str_to_bool(
                request.REQUEST.get('all_versions', 'False'))

            return _delete_item_at_location(old_location, delete_children,
                                            delete_all_versions)
        else:  # Since we have a course_id, we are updating an existing xblock.
            return _save_item(
                request,
                locator,
                old_location,
                data=request.json.get('data'),
                children=request.json.get('children'),
                metadata=request.json.get('metadata'),
                nullout=request.json.get('nullout'),
                grader_type=request.json.get('graderType'),
                publish=request.json.get('publish'),
            )
    elif request.method in ('PUT', 'POST'):
        return _create_item(request)
    else:
        return HttpResponseBadRequest(
            "Only instance creation is supported without a course_id.",
            content_type="text/plain")
Example #30
0
    def translate_locator_to_location(self, locator, get_course=False):
        """
        Returns an old style Location for the given Locator if there's an appropriate entry in the
        mapping collection. Note, it requires that the course was previously mapped (a side effect of
        translate_location or explicitly via create_map_entry) and
        the block's block_id was previously stored in the
        map (a side effect of translate_location or via add|update_block_location).

        If there are no matches, it returns None.

        Args:
            locator: a BlockUsageLocator to translate
            get_course: rather than finding the map for this locator, returns the CourseKey
                for the mapped course.
        """
        if get_course:
            cached_value = self._get_course_location_from_cache(
                # if locator is already a course_key it won't have course_key attr
                getattr(locator, 'course_key', locator))
        else:
            cached_value = self._get_location_from_cache(locator)
        if cached_value:
            return cached_value

        # migrate any records which don't have the org and offering fields as
        # this won't be able to find what it wants. (only needs to be run once ever per db,
        # I'm not sure how to control that, but I'm putting some check here for once per launch)
        if not getattr(self, 'offering_migrated', False):
            obsolete = self.location_map.find({
                'org': {
                    "$exists": False
                },
                "offering": {
                    "$exists": False
                },
            })
            self._migrate_if_necessary(obsolete)
            setattr(self, 'offering_migrated', True)

        entry = self.location_map.find_one(
            bson.son.SON([
                ('org', locator.org),
                ('offering', locator.offering),
            ]))

        # look for one which maps to this block block_id
        if entry is None:
            return None
        old_course_id = self._generate_location_course_id(entry['_id'])
        if get_course:
            return old_course_id

        for old_name, cat_to_usage in entry['block_map'].iteritems():
            for category, block_id in cat_to_usage.iteritems():
                # cache all entries and then figure out if we have the one we want
                # Always return revision=None because the
                # old draft module store wraps locations as draft before
                # trying to access things.
                location = old_course_id.make_usage_key(
                    category, self.decode_key_from_mongo(old_name))

                entry_org = "org"
                entry_offering = "offering"

                published_locator = BlockUsageLocator(CourseLocator(
                    org=entry[entry_org],
                    offering=entry[entry_offering],
                    branch=entry['prod_branch']),
                                                      block_type=category,
                                                      block_id=block_id)
                draft_locator = BlockUsageLocator(CourseLocator(
                    org=entry[entry_org],
                    offering=entry[entry_offering],
                    branch=entry['draft_branch']),
                                                  block_type=category,
                                                  block_id=block_id)
                self._cache_location_map_entry(location, published_locator,
                                               draft_locator)

                if block_id == locator.block_id:
                    return location

        return None
def export_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for exporting a course.

    GET
        html: return html page for import page
        application/x-tgz: return tar.gz file containing exported course
        json: not supported

    Note that there are 2 ways to request the tar.gz file. The request header can specify
    application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz).

    If the tar.gz file has been requested but the export operation fails, an HTML page will be returned
    which describes the error.
    """
    location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
    if not has_course_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)
    course_module = modulestore().get_item(old_location)

    # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
    requested_format = request.REQUEST.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html'))

    export_url = location.url_reverse('export') + '?_accept=application/x-tgz'
    if 'application/x-tgz' in requested_format:
        name = old_location.name
        export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
        root_dir = path(mkdtemp())

        try:
            export_to_xml(modulestore('direct'), contentstore(), old_location, root_dir, name, modulestore())

            logging.debug('tar file being generated at {0}'.format(export_file.name))
            with tarfile.open(name=export_file.name, mode='w:gz') as tar_file:
                tar_file.add(root_dir / name, arcname=name)
        except SerializationError, e:
            logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
            unit = None
            failed_item = None
            parent = None
            try:
                failed_item = modulestore().get_instance(course_module.location.course_id, e.location)
                parent_locs = modulestore().get_parent_locations(failed_item.location, course_module.location.course_id)

                if len(parent_locs) > 0:
                    parent = modulestore().get_item(parent_locs[0])
                    if parent.location.category == 'vertical':
                        unit = parent
            except:
                # if we have a nested exception, then we'll show the more generic error message
                pass

            unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True)

            return render_to_response('export.html', {
                'context_course': course_module,
                'in_err': True,
                'raw_err_msg': str(e),
                'failed_module': failed_item,
                'unit': unit,
                'edit_unit_url': unit_locator.url_reverse("unit") if parent else "",
                'course_home_url': location.url_reverse("course"),
                'export_url': export_url
            })
        except Exception, e:
            logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
            return render_to_response('export.html', {
                'context_course': course_module,
                'in_err': True,
                'unit': None,
                'raw_err_msg': str(e),
                'course_home_url': location.url_reverse("course"),
                'export_url': export_url
            })
Example #32
0
    def test_block_generations(self):
        """
        Test get_block_generations
        """
        test_course = persistent_factories.PersistentCourseFactory.create(
            org='testx',
            prettyid='edu.harvard.history.hist101',
            display_name='history test course',
            user_id='testbot')
        chapter = persistent_factories.ItemFactory.create(
            display_name='chapter 1',
            parent_location=test_course.location,
            user_id='testbot')
        sub = persistent_factories.ItemFactory.create(
            display_name='subsection 1',
            parent_location=chapter.location,
            user_id='testbot',
            category='vertical')
        first_problem = persistent_factories.ItemFactory.create(
            display_name='problem 1',
            parent_location=sub.location,
            user_id='testbot',
            category='problem',
            data="<problem></problem>")
        first_problem.max_attempts = 3
        first_problem.save()  # decache the above into the kvs
        updated_problem = modulestore('split').update_item(
            first_problem, 'testbot')
        self.assertIsNotNone(updated_problem.previous_version)
        self.assertEqual(updated_problem.previous_version,
                         first_problem.update_version)
        self.assertNotEqual(updated_problem.update_version,
                            first_problem.update_version)
        updated_loc = modulestore('split').delete_item(
            updated_problem.location, 'testbot', delete_children=True)

        second_problem = persistent_factories.ItemFactory.create(
            display_name='problem 2',
            parent_location=BlockUsageLocator(updated_loc,
                                              usage_id=sub.location.usage_id),
            user_id='testbot',
            category='problem',
            data="<problem></problem>")

        # course root only updated 2x
        version_history = modulestore('split').get_block_generations(
            test_course.location)
        self.assertEqual(version_history.locator.version_guid,
                         test_course.location.version_guid)
        self.assertEqual(len(version_history.children), 1)
        self.assertEqual(version_history.children[0].children, [])
        self.assertEqual(version_history.children[0].locator.version_guid,
                         chapter.location.version_guid)

        # sub changed on add, add problem, delete problem, add problem in strict linear seq
        version_history = modulestore('split').get_block_generations(
            sub.location)
        self.assertEqual(len(version_history.children), 1)
        self.assertEqual(len(version_history.children[0].children), 1)
        self.assertEqual(len(version_history.children[0].children[0].children),
                         1)
        self.assertEqual(
            len(version_history.children[0].children[0].children[0].children),
            0)

        # first and second problem may show as same usage_id; so, need to ensure their histories are right
        version_history = modulestore('split').get_block_generations(
            updated_problem.location)
        self.assertEqual(version_history.locator.version_guid,
                         first_problem.location.version_guid)
        self.assertEqual(len(version_history.children),
                         1)  # updated max_attempts
        self.assertEqual(len(version_history.children[0].children), 0)

        version_history = modulestore('split').get_block_generations(
            second_problem.location)
        self.assertNotEqual(version_history.locator.version_guid,
                            first_problem.location.version_guid)
Example #33
0
def export_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for exporting a course.

    GET
        html: return html page for import page
        application/x-tgz: return tar.gz file containing exported course
        json: not supported

    Note that there are 2 ways to request the tar.gz file. The request header can specify
    application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz).

    If the tar.gz file has been requested but the export operation fails, an HTML page will be returned
    which describes the error.
    """
    location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    if not has_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)
    course_module = modulestore().get_item(old_location)

    # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
    requested_format = request.REQUEST.get("_accept", request.META.get("HTTP_ACCEPT", "text/html"))

    export_url = location.url_reverse("export") + "?_accept=application/x-tgz"
    if "application/x-tgz" in requested_format:
        name = old_location.name
        export_file = NamedTemporaryFile(prefix=name + ".", suffix=".tar.gz")
        root_dir = path(mkdtemp())

        try:
            export_to_xml(modulestore("direct"), contentstore(), old_location, root_dir, name, modulestore())

        except SerializationError, e:
            logging.exception("There was an error exporting course {0}. {1}".format(course_module.location, unicode(e)))
            unit = None
            failed_item = None
            parent = None
            try:
                failed_item = modulestore().get_instance(course_module.location.course_id, e.location)
                parent_locs = modulestore().get_parent_locations(failed_item.location, course_module.location.course_id)

                if len(parent_locs) > 0:
                    parent = modulestore().get_item(parent_locs[0])
                    if parent.location.category == "vertical":
                        unit = parent
            except:
                # if we have a nested exception, then we'll show the more generic error message
                pass

            unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True)

            return render_to_response(
                "export.html",
                {
                    "context_course": course_module,
                    "in_err": True,
                    "raw_err_msg": str(e),
                    "failed_module": failed_item,
                    "unit": unit,
                    "edit_unit_url": unit_locator.url_reverse("unit") if parent else "",
                    "course_home_url": location.url_reverse("course"),
                    "export_url": export_url,
                },
            )
        except Exception, e:
            logging.exception("There was an error exporting course {0}. {1}".format(course_module.location, unicode(e)))
            return render_to_response(
                "export.html",
                {
                    "context_course": course_module,
                    "in_err": True,
                    "unit": None,
                    "raw_err_msg": str(e),
                    "course_home_url": location.url_reverse("course"),
                    "export_url": export_url,
                },
            )
    def test_translate_locator(self):
        """
        tests translate_locator_to_location(BlockUsageLocator)
        """
        # lookup for non-existent course
        org = 'foo_org'
        course = 'bar_course'
        new_style_package_id = '{}.geek_dept.{}.baz_run'.format(org, course)
        prob_locator = BlockUsageLocator(package_id=new_style_package_id,
                                         block_id='problem2',
                                         branch='published')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertIsNone(prob_location, 'found entry in empty map table')

        loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                               'baz_run'),
                                      new_style_package_id,
                                      block_map={
                                          'abc123': {
                                              'problem': 'problem2'
                                          },
                                          '48f23a10395384929234': {
                                              'chapter': 'chapter48f'
                                          },
                                          'baz_run': {
                                              'course': 'root'
                                          },
                                      })
        # only one course matches
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        # default branch
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', None))
        # test get_course keyword
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator, get_course=True)
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'course', 'baz_run', None))
        # explicit branch
        prob_locator = BlockUsageLocator(package_id=prob_locator.package_id,
                                         branch='draft',
                                         block_id=prob_locator.block_id)
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        # Even though the problem was set as draft, we always return revision=None to work
        # with old mongo/draft modulestores.
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', None))
        prob_locator = BlockUsageLocator(package_id=new_style_package_id,
                                         block_id='problem2',
                                         branch='production')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', None))
        # same for chapter except chapter cannot be draft in old system
        chap_locator = BlockUsageLocator(package_id=new_style_package_id,
                                         block_id='chapter48f',
                                         branch='production')
        chap_location = loc_mapper().translate_locator_to_location(
            chap_locator)
        self.assertEqual(
            chap_location,
            Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        # explicit branch
        chap_locator.branch = 'draft'
        chap_location = loc_mapper().translate_locator_to_location(
            chap_locator)
        self.assertEqual(
            chap_location,
            Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        chap_locator = BlockUsageLocator(package_id=new_style_package_id,
                                         block_id='chapter48f',
                                         branch='production')
        chap_location = loc_mapper().translate_locator_to_location(
            chap_locator)
        self.assertEqual(
            chap_location,
            Location('i4x', org, course, 'chapter', '48f23a10395384929234'))

        # look for non-existent problem
        prob_locator2 = BlockUsageLocator(package_id=new_style_package_id,
                                          branch='draft',
                                          block_id='problem3')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator2)
        self.assertIsNone(prob_location, 'Found non-existent problem')

        # add a distractor course
        new_style_package_id = '{}.geek_dept.{}.{}'.format(
            org, course, 'delta_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'delta_run'),
            new_style_package_id,
            block_map={'abc123': {
                'problem': 'problem3'
            }})
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertEqual(
            prob_location,
            Location('i4x', org, course, 'problem', 'abc123', None))

        # add a default course pointing to the delta_run
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'problem', '789abc123efg456'),
            new_style_package_id,
            block_map={'abc123': {
                'problem': 'problem3'
            }})
        # now query delta (2 entries point to it)
        prob_locator = BlockUsageLocator(package_id=new_style_package_id,
                                         branch='production',
                                         block_id='problem3')
        prob_location = loc_mapper().translate_locator_to_location(
            prob_locator)
        self.assertEqual(prob_location,
                         Location('i4x', org, course, 'problem', 'abc123'))
Example #35
0
 def get_old_id(self, locator):
     """
     Converts new locator to old id format (forcing to non-draft).
     """
     return loc_mapper().translate_locator_to_location(
         BlockUsageLocator(locator)).replace(revision=None)
Example #36
0
 def get_location_for_tab(tab):
     """  Returns the location (old-style) for a tab. """
     return loc_mapper().translate_locator_to_location(
         BlockUsageLocator(tab))
Example #37
0
 def test_repr(self):
     testurn = 'mit.eecs.6002x' + BRANCH_PREFIX + 'published' + BLOCK_PREFIX + 'HW3'
     testobj = BlockUsageLocator(course_id=testurn)
     self.assertEqual('BlockUsageLocator("mit.eecs.6002x/branch/published/block/HW3")', repr(testobj))
Example #38
0
def checklists_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, checklist_index=None):
    """
    The restful handler for checklists.

    GET
        html: return html page for all checklists
        json: return json representing all checklists. checklist_index is not supported for GET at this time.
    POST or PUT
        json: updates the checked state for items within a particular checklist. checklist_index is required.
    """
    location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
    if not has_course_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)

    modulestore = get_modulestore(old_location)
    course_module = modulestore.get_item(old_location)

    json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json')
    if request.method == 'GET':
        # If course was created before checklists were introduced, copy them over
        # from the template.
        if not course_module.checklists:
            course_module.checklists = CourseDescriptor.checklists.default
            modulestore.update_item(course_module, request.user.id)

        expanded_checklists = expand_all_action_urls(course_module)
        if json_request:
            return JsonResponse(expanded_checklists)
        else:
            handler_url = location.url_reverse('checklists/', '')
            return render_to_response('checklists.html',
                                      {
                                          'handler_url': handler_url,
                                          # context_course is used by analytics
                                          'context_course': course_module,
                                          'checklists': expanded_checklists
                                      })
    elif json_request:
        # Can now assume POST or PUT because GET handled above.
        if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
            index = int(checklist_index)
            persisted_checklist = course_module.checklists[index]
            modified_checklist = json.loads(request.body)
            # Only thing the user can modify is the "checked" state.
            # We don't want to persist what comes back from the client because it will
            # include the expanded action URLs (which are non-portable).
            for item_index, item in enumerate(modified_checklist.get('items')):
                persisted_checklist['items'][item_index]['is_checked'] = item['is_checked']
            # seeming noop which triggers kvs to record that the metadata is
            # not default
            course_module.checklists = course_module.checklists
            course_module.save()
            modulestore.update_item(course_module, request.user.id)
            expanded_checklist = expand_checklist_action_url(course_module, persisted_checklist)
            return JsonResponse(expanded_checklist)
        else:
            return HttpResponseBadRequest(
                ("Could not save checklist state because the checklist index "
                 "was out of range or unspecified."),
                content_type="text/plain"
            )
    else:
        return HttpResponseNotFound()
Example #39
0
def export_handler(request,
                   tag=None,
                   package_id=None,
                   branch=None,
                   version_guid=None,
                   block=None):
    """
    The restful handler for exporting a course.

    GET
        html: return html page for import page
        application/x-tgz: return tar.gz file containing exported course
        json: not supported

    Note that there are 2 ways to request the tar.gz file. The request header can specify
    application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz).

    If the tar.gz file has been requested but the export operation fails, an HTML page will be returned
    which describes the error.
    """
    location = BlockUsageLocator(package_id=package_id,
                                 branch=branch,
                                 version_guid=version_guid,
                                 block_id=block)
    if not has_course_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)
    course_module = modulestore().get_item(old_location)

    # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
    requested_format = request.REQUEST.get(
        '_accept', request.META.get('HTTP_ACCEPT', 'text/html'))

    export_url = location.url_reverse('export') + '?_accept=application/x-tgz'
    if 'application/x-tgz' in requested_format:
        name = old_location.name
        export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
        root_dir = path(mkdtemp())

        try:
            export_to_xml(modulestore('direct'), contentstore(), old_location,
                          root_dir, name, modulestore())

        except SerializationError, e:
            logging.exception(
                'There was an error exporting course {0}. {1}'.format(
                    course_module.location, unicode(e)))
            unit = None
            failed_item = None
            parent = None
            try:
                failed_item = modulestore().get_instance(
                    course_module.location.course_id, e.location)
                parent_locs = modulestore().get_parent_locations(
                    failed_item.location, course_module.location.course_id)

                if len(parent_locs) > 0:
                    parent = modulestore().get_item(parent_locs[0])
                    if parent.location.category == 'vertical':
                        unit = parent
            except:
                # if we have a nested exception, then we'll show the more generic error message
                pass

            unit_locator = loc_mapper().translate_location(
                old_location.course_id, parent.location, False, True)

            return render_to_response(
                'export.html', {
                    'context_course':
                    course_module,
                    'in_err':
                    True,
                    'raw_err_msg':
                    str(e),
                    'failed_module':
                    failed_item,
                    'unit':
                    unit,
                    'edit_unit_url':
                    unit_locator.url_reverse("unit") if parent else "",
                    'course_home_url':
                    location.url_reverse("course"),
                    'export_url':
                    export_url
                })
        except Exception, e:
            logging.exception(
                'There was an error exporting course {0}. {1}'.format(
                    course_module.location, unicode(e)))
            return render_to_response(
                'export.html', {
                    'context_course': course_module,
                    'in_err': True,
                    'unit': None,
                    'raw_err_msg': str(e),
                    'course_home_url': location.url_reverse("course"),
                    'export_url': export_url
                })
Example #40
0
def import_handler(request,
                   tag=None,
                   package_id=None,
                   branch=None,
                   version_guid=None,
                   block=None):
    """
    The restful handler for importing a course.

    GET
        html: return html page for import page
        json: not supported
    POST or PUT
        json: import a course via the .tar.gz file specified in request.FILES
    """
    location = BlockUsageLocator(package_id=package_id,
                                 branch=branch,
                                 version_guid=version_guid,
                                 block_id=block)
    if not has_course_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)

    if 'application/json' in request.META.get('HTTP_ACCEPT',
                                              'application/json'):
        if request.method == 'GET':
            raise NotImplementedError('coming soon')
        else:
            data_root = path(settings.GITHUB_REPO_ROOT)
            course_subdir = "{0}-{1}-{2}".format(old_location.org,
                                                 old_location.course,
                                                 old_location.name)
            course_dir = data_root / course_subdir

            filename = request.FILES['course-data'].name
            if not filename.endswith('.tar.gz'):
                return JsonResponse(
                    {
                        'ErrMsg':
                        _('We only support uploading a .tar.gz file.'),
                        'Stage': 1
                    },
                    status=415)
            temp_filepath = course_dir / filename

            if not course_dir.isdir():
                os.mkdir(course_dir)

            logging.debug('importing course to {0}'.format(temp_filepath))

            # Get upload chunks byte ranges
            try:
                matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"])
                content_range = matches.groupdict()
            except KeyError:  # Single chunk
                # no Content-Range header, so make one that will work
                content_range = {'start': 0, 'stop': 1, 'end': 2}

            # stream out the uploaded files in chunks to disk
            if int(content_range['start']) == 0:
                mode = "wb+"
            else:
                mode = "ab+"
                size = os.path.getsize(temp_filepath)
                # Check to make sure we haven't missed a chunk
                # This shouldn't happen, even if different instances are handling
                # the same session, but it's always better to catch errors earlier.
                if size < int(content_range['start']):
                    log.warning(
                        "Reported range %s does not match size downloaded so far %s",
                        content_range['start'], size)
                    return JsonResponse(
                        {
                            'ErrMsg':
                            _('File upload corrupted. Please try again'),
                            'Stage': 1
                        },
                        status=409)
                # The last request sometimes comes twice. This happens because
                # nginx sends a 499 error code when the response takes too long.
                elif size > int(content_range['stop']) and size == int(
                        content_range['end']):
                    return JsonResponse({'ImportStatus': 1})

            with open(temp_filepath, mode) as temp_file:
                for chunk in request.FILES['course-data'].chunks():
                    temp_file.write(chunk)

            size = os.path.getsize(temp_filepath)

            if int(content_range['stop']) != int(content_range['end']) - 1:
                # More chunks coming
                return JsonResponse({
                    "files": [{
                        "name": filename,
                        "size": size,
                        "deleteUrl": "",
                        "deleteType": "",
                        "url": location.url_reverse('import'),
                        "thumbnailUrl": ""
                    }]
                })

            else:  # This was the last chunk.

                # Use sessions to keep info about import progress
                session_status = request.session.setdefault(
                    "import_status", {})
                key = location.package_id + filename
                session_status[key] = 1
                request.session.modified = True

                # Do everything from now on in a try-finally block to make sure
                # everything is properly cleaned up.
                try:

                    tar_file = tarfile.open(temp_filepath)
                    try:
                        safetar_extractall(tar_file,
                                           (course_dir + '/').encode('utf-8'))
                    except SuspiciousOperation as exc:
                        return JsonResponse(
                            {
                                'ErrMsg': 'Unsafe tar file. Aborting import.',
                                'SuspiciousFileOperationMsg': exc.args[0],
                                'Stage': 1
                            },
                            status=400)
                    finally:
                        tar_file.close()

                    session_status[key] = 2
                    request.session.modified = True

                    # find the 'course.xml' file
                    def get_all_files(directory):
                        """
                        For each file in the directory, yield a 2-tuple of (file-name,
                        directory-path)
                        """
                        for dirpath, _dirnames, filenames in os.walk(
                                directory):
                            for filename in filenames:
                                yield (filename, dirpath)

                    def get_dir_for_fname(directory, filename):
                        """
                        Returns the dirpath for the first file found in the directory
                        with the given name.  If there is no file in the directory with
                        the specified name, return None.
                        """
                        for fname, dirpath in get_all_files(directory):
                            if fname == filename:
                                return dirpath
                        return None

                    fname = "course.xml"

                    dirpath = get_dir_for_fname(course_dir, fname)

                    if not dirpath:
                        return JsonResponse(
                            {
                                'ErrMsg':
                                _('Could not find the course.xml file in the package.'
                                  ),
                                'Stage':
                                2
                            },
                            status=415)

                    logging.debug('found course.xml at {0}'.format(dirpath))

                    if dirpath != course_dir:
                        for fname in os.listdir(dirpath):
                            shutil.move(dirpath / fname, course_dir)

                    _module_store, course_items = import_from_xml(
                        modulestore('direct'),
                        settings.GITHUB_REPO_ROOT, [course_subdir],
                        load_error_modules=False,
                        static_content_store=contentstore(),
                        target_location_namespace=old_location,
                        draft_store=modulestore())

                    new_location = course_items[0].location
                    logging.debug('new course at {0}'.format(new_location))

                    session_status[key] = 3
                    request.session.modified = True

                    auth.add_users(request.user,
                                   CourseInstructorRole(new_location),
                                   request.user)
                    auth.add_users(request.user, CourseStaffRole(new_location),
                                   request.user)
                    logging.debug('created all course groups at {0}'.format(
                        new_location))

                # Send errors to client with stage at which error occurred.
                except Exception as exception:  # pylint: disable=W0703
                    return JsonResponse(
                        {
                            'ErrMsg': str(exception),
                            'Stage': session_status[key]
                        },
                        status=400)

                finally:
                    shutil.rmtree(course_dir)

                return JsonResponse({'Status': 'OK'})
    elif request.method == 'GET':  # assume html
        course_module = modulestore().get_item(old_location)
        return render_to_response(
            'import.html', {
                'context_course':
                course_module,
                'successful_import_redirect_url':
                location.url_reverse("course"),
                'import_status_url':
                location.url_reverse("import_status", "fillerName"),
            })
    else:
        return HttpResponseNotFound()
Example #41
0
def xblock_view_handler(request,
                        package_id,
                        view_name,
                        tag=None,
                        branch=None,
                        version_guid=None,
                        block=None):
    """
    The restful handler for requests for rendered xblock views.

    Returns a json object containing two keys:
        html: The rendered html of the view
        resources: A list of tuples where the first element is the resource hash, and
            the second is the resource description
    """
    locator = BlockUsageLocator(package_id=package_id,
                                branch=branch,
                                version_guid=version_guid,
                                block_id=block)
    if not has_course_access(request.user, locator):
        raise PermissionDenied()
    old_location = loc_mapper().translate_locator_to_location(locator)

    accept_header = request.META.get('HTTP_ACCEPT', 'application/json')

    if 'application/json' in accept_header:
        store = get_modulestore(old_location)
        component = store.get_item(old_location)

        # wrap the generated fragment in the xmodule_editor div so that the javascript
        # can bind to it correctly
        component.runtime.wrappers.append(partial(wrap_xblock,
                                                  'StudioRuntime'))

        if view_name == 'studio_view':
            try:
                fragment = component.render('studio_view')
            # catch exceptions indiscriminately, since after this point they escape the
            # dungeon and surface as uneditable, unsaveable, and undeletable
            # component-goblins.
            except Exception as exc:  # pylint: disable=w0703
                log.debug("unable to render studio_view for %r",
                          component,
                          exc_info=True)
                fragment = Fragment(
                    render_to_string('html_error.html', {'message': str(exc)}))

            store.save_xmodule(component)
        elif view_name == 'student_view' and component.has_children:
            # For non-leaf xblocks on the unit page, show the special rendering
            # which links to the new container page.
            course_location = loc_mapper().translate_locator_to_location(
                locator, True)
            course = store.get_item(course_location)
            html = render_to_string('unit_container_xblock_component.html', {
                'course': course,
                'xblock': component,
                'locator': locator
            })
            return JsonResponse({
                'html': html,
                'resources': [],
            })
        elif view_name in ('student_view', 'container_preview'):
            is_container_view = (view_name == 'container_preview')

            # Only show the new style HTML for the container view, i.e. for non-verticals
            # Note: this special case logic can be removed once the unit page is replaced
            # with the new container view.
            is_read_only_view = is_container_view
            context = {
                'container_view': is_container_view,
                'read_only': is_read_only_view,
                'root_xblock': component
            }

            fragment = get_preview_fragment(request, component, context)
            # For old-style pages (such as unit and static pages), wrap the preview with
            # the component div. Note that the container view recursively adds headers
            # into the preview fragment, so we don't want to add another header here.
            if not is_container_view:
                fragment.content = render_to_string(
                    'component.html',
                    {
                        'preview':
                        fragment.content,
                        'label':
                        component.display_name
                        or component.scope_ids.block_type,

                        # Native XBlocks are responsible for persisting their own data,
                        # so they are also responsible for providing save/cancel buttons.
                        'show_save_cancel':
                        isinstance(component,
                                   xmodule.x_module.XModuleDescriptor),
                    })
        else:
            raise Http404

        hashed_resources = OrderedDict()
        for resource in fragment.resources:
            hashed_resources[hash_resource(resource)] = resource

        return JsonResponse({
            'html': fragment.content,
            'resources': hashed_resources.items()
        })

    else:
        return HttpResponse(status=406)
 def test_update_block(self):
     """
     test update_block_location_translator(location, usage_id, old_course_id=None)
     """
     org = 'foo_org'
     course = 'bar_course'
     new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'baz_run'),
                                   new_style_course_id,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem2'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48f'
                                       },
                                       '1': {
                                           'chapter': 'chapter1',
                                           'problem': 'problem1'
                                       },
                                   })
     new_style_course_id2 = '{}.geek_dept.{}.delta_run'.format(org, course)
     loc_mapper().create_map_entry(Location('i4x', org, course, 'course',
                                            'delta_run'),
                                   new_style_course_id2,
                                   block_map={
                                       'abc123': {
                                           'problem': 'problem3'
                                       },
                                       '48f23a10395384929234': {
                                           'chapter': 'chapter48b'
                                       },
                                       '1': {
                                           'chapter': 'chapter2',
                                           'problem': 'problem2'
                                       },
                                   })
     location = Location('i4x', org, course, 'problem', '1')
     # change in all courses to same value
     loc_mapper().update_block_location_translator(location, 'problem1')
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id,
                           usage_id='problem1'))
     self.assertEqual(trans_loc, location)
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id2,
                           usage_id='problem1'))
     self.assertEqual(trans_loc, location)
     # try to change to overwrite used usage_id
     location = Location('i4x', org, course, 'chapter',
                         '48f23a10395384929234')
     with self.assertRaises(DuplicateItemError):
         loc_mapper().update_block_location_translator(location, 'chapter2')
     # just change the one course
     loc_mapper().update_block_location_translator(
         location, 'chapter2', '{}/{}/{}'.format(org, course, 'baz_run'))
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id,
                           usage_id='chapter2'))
     self.assertEqual(trans_loc.name, '48f23a10395384929234')
     # but this still points to the old
     trans_loc = loc_mapper().translate_locator_to_location(
         BlockUsageLocator(course_id=new_style_course_id2,
                           usage_id='chapter2'))
     self.assertEqual(trans_loc.name, '1')
Example #43
0
def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for unit-specific requests.

    GET
        html: return html page for editing a unit
        json: not currently supported
    """
    if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
        locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
        try:
            old_location, course, item, lms_link = _get_item_in_course(request, locator)
        except ItemNotFoundError:
            return HttpResponseBadRequest()

        component_templates = defaultdict(list)
        for category in COMPONENT_TYPES:
            component_class = _load_mixed_class(category)
            # add the default template
            # TODO: Once mixins are defined per-application, rather than per-runtime,
            # this should use a cms mixed-in class. (cpennington)
            if hasattr(component_class, 'display_name'):
                display_name = component_class.display_name.default or 'Blank'
            else:
                display_name = 'Blank'
            component_templates[category].append((
                display_name,
                category,
                False,  # No defaults have markdown (hardcoded current default)
                None  # no boilerplate for overrides
            ))
            # add boilerplates
            if hasattr(component_class, 'templates'):
                for template in component_class.templates():
                    filter_templates = getattr(component_class, 'filter_templates', None)
                    if not filter_templates or filter_templates(template, course):
                        component_templates[category].append((
                            template['metadata'].get('display_name'),
                            category,
                            template['metadata'].get('markdown') is not None,
                            template.get('template_id')
                        ))

        # Check if there are any advanced modules specified in the course policy.
        # These modules should be specified as a list of strings, where the strings
        # are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
        # enabled for the course.
        course_advanced_keys = course.advanced_modules

        # Set component types according to course policy file
        if isinstance(course_advanced_keys, list):
            for category in course_advanced_keys:
                if category in ADVANCED_COMPONENT_TYPES:
                    # Do I need to allow for boilerplates or just defaults on the
                    # class? i.e., can an advanced have more than one entry in the
                    # menu? one for default and others for prefilled boilerplates?
                    try:
                        component_class = _load_mixed_class(category)

                        component_templates['advanced'].append(
                            (
                                component_class.display_name.default or category,
                                category,
                                False,
                                None  # don't override default data
                            )
                        )
                    except PluginMissingError:
                        # dhm: I got this once but it can happen any time the
                        # course author configures an advanced component which does
                        # not exist on the server. This code here merely
                        # prevents any authors from trying to instantiate the
                        # non-existent component type by not showing it in the menu
                        pass
        else:
            log.error(
                "Improper format for course advanced keys! %s",
                course_advanced_keys
            )

        components = [
            loc_mapper().translate_location(
                course.location.course_id, component.location, False, True
            )
            for component
            in item.get_children()
        ]

        # TODO (cpennington): If we share units between courses,
        # this will need to change to check permissions correctly so as
        # to pick the correct parent subsection

        containing_subsection_locs = modulestore().get_parent_locations(old_location, None)
        containing_subsection = modulestore().get_item(containing_subsection_locs[0])
        containing_section_locs = modulestore().get_parent_locations(
            containing_subsection.location, None
        )
        containing_section = modulestore().get_item(containing_section_locs[0])

        # cdodge hack. We're having trouble previewing drafts via jump_to redirect
        # so let's generate the link url here

        # need to figure out where this item is in the list of children as the
        # preview will need this
        index = 1
        for child in containing_subsection.get_children():
            if child.location == item.location:
                break
            index = index + 1

        preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')

        preview_lms_link = (
            u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'
        ).format(
            preview_lms_base=preview_lms_base,
            lms_base=settings.LMS_BASE,
            org=course.location.org,
            course=course.location.course,
            course_name=course.location.name,
            section=containing_section.location.name,
            subsection=containing_subsection.location.name,
            index=index
        )

        return render_to_response('unit.html', {
            'context_course': course,
            'unit': item,
            'unit_locator': locator,
            'components': components,
            'component_templates': component_templates,
            'draft_preview_link': preview_lms_link,
            'published_preview_link': lms_link,
            'subsection': containing_subsection,
            'release_date': (
                get_default_time_display(containing_subsection.start)
                if containing_subsection.start is not None else None
            ),
            'section': containing_section,
            'new_unit_category': 'vertical',
            'unit_state': compute_unit_state(item),
            'published_date': (
                get_default_time_display(item.published_date)
                if item.published_date is not None else None
            ),
        })
    else:
        return HttpResponseBadRequest("Only supports html requests")
Example #44
0
def subsection_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for subsection-specific requests.

    GET
        html: return html page for editing a subsection
        json: not currently supported
    """
    if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
        locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
        try:
            old_location, course, item, lms_link = _get_item_in_course(request, locator)
        except ItemNotFoundError:
            return HttpResponseBadRequest()

        preview_link = get_lms_link_for_item(old_location, course_id=course.location.course_id, preview=True)

        # make sure that location references a 'sequential', otherwise return
        # BadRequest
        if item.location.category != 'sequential':
            return HttpResponseBadRequest()

        parent_locs = modulestore().get_parent_locations(old_location, None)

        # we're for now assuming a single parent
        if len(parent_locs) != 1:
            logging.error(
                'Multiple (or none) parents have been found for %s',
                unicode(locator)
            )

        # this should blow up if we don't find any parents, which would be erroneous
        parent = modulestore().get_item(parent_locs[0])

        # remove all metadata from the generic dictionary that is presented in a
        # more normalized UI. We only want to display the XBlocks fields, not
        # the fields from any mixins that have been added
        fields = getattr(item, 'unmixed_class', item.__class__).fields

        policy_metadata = dict(
            (field.name, field.read_from(item))
            for field
            in fields.values()
            if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
        )

        can_view_live = False
        subsection_units = item.get_children()
        for unit in subsection_units:
            state = compute_unit_state(unit)
            if state == UnitState.public or state == UnitState.draft:
                can_view_live = True
                break

        course_locator = loc_mapper().translate_location(
            course.location.course_id, course.location, False, True
        )

        return render_to_response(
            'edit_subsection.html',
            {
                'subsection': item,
                'context_course': course,
                'new_unit_category': 'vertical',
                'lms_link': lms_link,
                'preview_link': preview_link,
                'course_graders': json.dumps(CourseGradingModel.fetch(course_locator).graders),
                'parent_item': parent,
                'locator': locator,
                'policy_metadata': policy_metadata,
                'subsection_units': subsection_units,
                'can_view_live': can_view_live
            }
        )
    else:
        return HttpResponseBadRequest("Only supports html requests")
Example #45
0
def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None,
               grader_type=None, publish=None):
    """
    Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
    nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
    to default).

    The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator
    """
    store = get_modulestore(item_location)

    try:
        existing_item = store.get_item(item_location)
    except ItemNotFoundError:
        if item_location.category in CREATE_IF_NOT_FOUND:
            # New module at this location, for pages that are not pre-created.
            # Used for course info handouts.
            store.create_and_save_xmodule(item_location)
            existing_item = store.get_item(item_location)
        else:
            raise
    except InvalidLocationError:
        log.error("Can't find item by location.")
        return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)

    old_metadata = own_metadata(existing_item)

    if publish:
        if publish == 'make_private':
            _xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
        elif publish == 'create_draft':
            # This clones the existing item location to a draft location (the draft is
            # implicit, because modulestore is a Draft modulestore)
            modulestore().convert_to_draft(item_location)

    if data:
        # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
        existing_item.data = data
    else:
        data = existing_item.get_explicitly_set_fields_by_scope(Scope.content)

    if children is not None:
        children_ids = [
            loc_mapper().translate_locator_to_location(BlockUsageLocator(child_locator)).url()
            for child_locator
            in children
        ]
        existing_item.children = children_ids

    # also commit any metadata which might have been passed along
    if nullout is not None or metadata is not None:
        # the postback is not the complete metadata, as there's system metadata which is
        # not presented to the end-user for editing. So let's use the original (existing_item) and
        # 'apply' the submitted metadata, so we don't end up deleting system metadata.
        if nullout is not None:
            for metadata_key in nullout:
                setattr(existing_item, metadata_key, None)

        # update existing metadata with submitted metadata (which can be partial)
        # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
        # the intent is to make it None, use the nullout field
        if metadata is not None:
            for metadata_key, value in metadata.items():
                field = existing_item.fields[metadata_key]

                if value is None:
                    field.delete_from(existing_item)
                else:
                    try:
                        value = field.from_json(value)
                    except ValueError:
                        return JsonResponse({"error": "Invalid data"}, 400)
                    field.write_to(existing_item, value)

        if existing_item.category == 'video':
            manage_video_subtitles_save(existing_item, request.user, old_metadata, generate_translation=True)

    # commit to datastore
    store.update_item(existing_item, request.user.id)

    result = {
        'id': unicode(usage_loc),
        'data': data,
        'metadata': own_metadata(existing_item)
    }

    if grader_type is not None:
        result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type, request.user))

    # Make public after updating the xblock, in case the caller asked
    # for both an update and a publish.
    if publish and publish == 'make_public':
        def _publish(block):
            # This is super gross, but prevents us from publishing something that
            # we shouldn't. Ideally, all modulestores would have a consistant
            # interface for publishing. However, as of now, only the DraftMongoModulestore
            # does, so we have to check for the attribute explicitly.
            store = get_modulestore(block.location)
            if hasattr(store, 'publish'):
                store.publish(block.location, request.user.id)

        _xmodule_recurse(
            existing_item,
            _publish
        )

    # Note that children aren't being returned until we have a use case.
    return JsonResponse(result)
Example #46
0
    def test_translate_locator(self):
        """
        tests translate_locator_to_location(BlockUsageLocator)
        """
        # lookup for non-existent course
        org = 'foo_org'
        course = 'bar_course'
        new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course)
        prob_locator = BlockUsageLocator(
            course_id=new_style_course_id,
            usage_id='problem2'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertIsNone(prob_location, 'found entry in empty map table')

        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'baz_run'),
            new_style_course_id,
            block_map={
                'abc123': {'problem': 'problem2'},
                '48f23a10395384929234': {'chapter': 'chapter48f'}
            }
        )
        # only one course matches
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        # default branch
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))
        # explicit branch
        prob_locator = BlockUsageLocator(prob_locator, branch='draft')
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', 'draft'))
        prob_locator = BlockUsageLocator(
            course_id=new_style_course_id, usage_id='problem2', branch='production'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))
        # same for chapter except chapter cannot be draft in old system
        chap_locator = BlockUsageLocator(
            course_id=new_style_course_id,
            usage_id='chapter48f'
        )
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        # explicit branch
        chap_locator = BlockUsageLocator(chap_locator, branch='draft')
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234'))
        chap_locator = BlockUsageLocator(
            course_id=new_style_course_id, usage_id='chapter48f', branch='production'
        )
        chap_location = loc_mapper().translate_locator_to_location(chap_locator)
        self.assertEqual(chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234'))

        # look for non-existent problem
        prob_locator2 = BlockUsageLocator(
            course_id=new_style_course_id,
            branch='draft',
            usage_id='problem3'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator2)
        self.assertIsNone(prob_location, 'Found non-existent problem')

        # add a distractor course
        new_style_course_id = '{}.geek_dept.{}.{}'.format(org, course, 'delta_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'delta_run'),
            new_style_course_id,
            block_map={'abc123': {'problem': 'problem3'}}
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))

        # add a default course pointing to the delta_run
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'problem', '789abc123efg456'),
            new_style_course_id,
            block_map={'abc123': {'problem': 'problem3'}}
        )
        # now query delta (2 entries point to it)
        prob_locator = BlockUsageLocator(
            course_id=new_style_course_id,
            branch='production',
            usage_id='problem3'
        )
        prob_location = loc_mapper().translate_locator_to_location(prob_locator)
        self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123'))
Example #47
0
 def _get_locator(self, resp):
     """ Returns the locator and old-style location (as a string) from the response returned by a create operation. """
     locator = json.loads(resp.content).get('locator')
     return locator, loc_mapper().translate_locator_to_location(
         BlockUsageLocator(locator)).url()
def import_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
    """
    The restful handler for importing a course.

    GET
        html: return html page for import page
        json: not supported
    POST or PUT
        json: import a course via the .tar.gz file specified in request.FILES
    """
    location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
    if not has_course_access(request.user, location):
        raise PermissionDenied()

    old_location = loc_mapper().translate_locator_to_location(location)

    if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
        if request.method == 'GET':
            raise NotImplementedError('coming soon')
        else:
            data_root = path(settings.GITHUB_REPO_ROOT)
            course_subdir = "{0}-{1}-{2}".format(old_location.org, old_location.course, old_location.name)
            course_dir = data_root / course_subdir

            filename = request.FILES['course-data'].name
            if not filename.endswith('.tar.gz'):
                return JsonResponse(
                    {
                        'ErrMsg': _('We only support uploading a .tar.gz file.'),
                        'Stage': 1
                    },
                    status=415
                )
            temp_filepath = course_dir / filename

            if not course_dir.isdir():
                os.mkdir(course_dir)

            logging.debug('importing course to {0}'.format(temp_filepath))

            # Get upload chunks byte ranges
            try:
                matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"])
                content_range = matches.groupdict()
            except KeyError:    # Single chunk
                # no Content-Range header, so make one that will work
                content_range = {'start': 0, 'stop': 1, 'end': 2}

            # stream out the uploaded files in chunks to disk
            if int(content_range['start']) == 0:
                mode = "wb+"
            else:
                mode = "ab+"
                size = os.path.getsize(temp_filepath)
                # Check to make sure we haven't missed a chunk
                # This shouldn't happen, even if different instances are handling
                # the same session, but it's always better to catch errors earlier.
                if size < int(content_range['start']):
                    log.warning(
                        "Reported range %s does not match size downloaded so far %s",
                        content_range['start'],
                        size
                    )
                    return JsonResponse(
                        {
                            'ErrMsg': _('File upload corrupted. Please try again'),
                            'Stage': 1
                        },
                        status=409
                    )
                # The last request sometimes comes twice. This happens because
                # nginx sends a 499 error code when the response takes too long.
                elif size > int(content_range['stop']) and size == int(content_range['end']):
                    return JsonResponse({'ImportStatus': 1})

            with open(temp_filepath, mode) as temp_file:
                for chunk in request.FILES['course-data'].chunks():
                    temp_file.write(chunk)

            size = os.path.getsize(temp_filepath)

            if int(content_range['stop']) != int(content_range['end']) - 1:
                # More chunks coming
                return JsonResponse({
                    "files": [{
                                  "name": filename,
                                  "size": size,
                                  "deleteUrl": "",
                                  "deleteType": "",
                                  "url": location.url_reverse('import'),
                                  "thumbnailUrl": ""
                              }]
                })

            else:   # This was the last chunk.

                # Use sessions to keep info about import progress
                session_status = request.session.setdefault("import_status", {})
                key = location.package_id + filename
                session_status[key] = 1
                request.session.modified = True

                # Do everything from now on in a try-finally block to make sure
                # everything is properly cleaned up.
                try:

                    tar_file = tarfile.open(temp_filepath)
                    try:
                        safetar_extractall(tar_file, (course_dir + '/').encode('utf-8'))
                    except SuspiciousOperation as exc:
                        return JsonResponse(
                            {
                                'ErrMsg': 'Unsafe tar file. Aborting import.',
                                'SuspiciousFileOperationMsg': exc.args[0],
                                'Stage': 1
                            },
                            status=400
                        )
                    finally:
                        tar_file.close()

                    session_status[key] = 2
                    request.session.modified = True

                    # find the 'course.xml' file
                    def get_all_files(directory):
                        """
                        For each file in the directory, yield a 2-tuple of (file-name,
                        directory-path)
                        """
                        for dirpath, _dirnames, filenames in os.walk(directory):
                            for filename in filenames:
                                yield (filename, dirpath)

                    def get_dir_for_fname(directory, filename):
                        """
                        Returns the dirpath for the first file found in the directory
                        with the given name.  If there is no file in the directory with
                        the specified name, return None.
                        """
                        for fname, dirpath in get_all_files(directory):
                            if fname == filename:
                                return dirpath
                        return None

                    fname = "course.xml"

                    dirpath = get_dir_for_fname(course_dir, fname)

                    if not dirpath:
                        return JsonResponse(
                            {

                                'ErrMsg': _('Could not find the course.xml file in the package.'),
                                'Stage': 2
                            },
                            status=415
                        )

                    logging.debug('found course.xml at {0}'.format(dirpath))

                    if dirpath != course_dir:
                        for fname in os.listdir(dirpath):
                            shutil.move(dirpath / fname, course_dir)

                    _module_store, course_items = import_from_xml(
                        modulestore('direct'),
                        settings.GITHUB_REPO_ROOT,
                        [course_subdir],
                        load_error_modules=False,
                        static_content_store=contentstore(),
                        target_location_namespace=old_location,
                        draft_store=modulestore()
                    )

                    new_location = course_items[0].location
                    logging.debug('new course at {0}'.format(new_location))

                    session_status[key] = 3
                    request.session.modified = True

                    auth.add_users(request.user, CourseInstructorRole(new_location), request.user)
                    auth.add_users(request.user, CourseStaffRole(new_location), request.user)
                    logging.debug('created all course groups at {0}'.format(new_location))

                # Send errors to client with stage at which error occurred.
                except Exception as exception:   # pylint: disable=W0703
                    return JsonResponse(
                        {
                            'ErrMsg': str(exception),
                            'Stage': session_status[key]
                        },
                        status=400
                    )

                finally:
                    shutil.rmtree(course_dir)

                return JsonResponse({'Status': 'OK'})
    elif request.method == 'GET':  # assume html
        course_module = modulestore().get_item(old_location)
        return render_to_response('import.html', {
            'context_course': course_module,
            'successful_import_redirect_url': location.url_reverse("course"),
            'import_status_url': location.url_reverse("import_status", "fillerName"),
        })
    else:
        return HttpResponseNotFound()
Example #49
0
    def test_add_block(self):
        """
        Test add_block_location_translator(location, old_course_id=None, usage_id=None)
        """
        # call w/ no matching courses
        org = 'foo_org'
        course = 'bar_course'
        old_style_course_id = '{}/{}/{}'.format(org, course, 'baz_run')
        problem_name = 'abc123abc123abc123abc123abc123f9'
        location = Location('i4x', org, course, 'problem', problem_name)
        with self.assertRaises(ItemNotFoundError):
            loc_mapper().add_block_location_translator(location)
        with self.assertRaises(ItemNotFoundError):
            loc_mapper().add_block_location_translator(location, old_style_course_id)

        # w/ one matching course
        new_style_course_id = '{}.{}.{}'.format(org, course, 'baz_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'baz_run'),
            new_style_course_id,
        )
        new_usage_id = loc_mapper().add_block_location_translator(location)
        self.assertEqual(new_usage_id, 'problemabc')
        # look it up
        translated_loc = loc_mapper().translate_location(old_style_course_id, location, add_entry_if_missing=False)
        self.assertEqual(translated_loc.course_id, new_style_course_id)
        self.assertEqual(translated_loc.usage_id, new_usage_id)

        # w/ one distractor which has one entry already
        new_style_course_id = '{}.geek_dept.{}.{}'.format(org, course, 'delta_run')
        loc_mapper().create_map_entry(
            Location('i4x', org, course, 'course', 'delta_run'),
            new_style_course_id,
            block_map={'48f23a10395384929234': {'chapter': 'chapter48f'}}
        )
        # try adding the one added before
        new_usage_id2 = loc_mapper().add_block_location_translator(location)
        self.assertEqual(new_usage_id, new_usage_id2)
        # it should be in the distractor now
        new_location = loc_mapper().translate_locator_to_location(
            BlockUsageLocator(course_id=new_style_course_id, usage_id=new_usage_id2)
        )
        self.assertEqual(new_location, location)
        # add one close to the existing chapter (cause name collision)
        location = Location('i4x', org, course, 'chapter', '48f23a103953849292341234567890ab')
        new_usage_id = loc_mapper().add_block_location_translator(location)
        self.assertRegexpMatches(new_usage_id, r'^chapter48f\d')
        # retrievable from both courses
        new_location = loc_mapper().translate_locator_to_location(
            BlockUsageLocator(course_id=new_style_course_id, usage_id=new_usage_id)
        )
        self.assertEqual(new_location, location)
        new_location = loc_mapper().translate_locator_to_location(
            BlockUsageLocator(course_id='{}.{}.{}'.format(org, course, 'baz_run'), usage_id=new_usage_id)
        )
        self.assertEqual(new_location, location)

        # provoke duplicate item errors
        location = location.replace(name='44f23a103953849292341234567890ab')
        with self.assertRaises(DuplicateItemError):
            loc_mapper().add_block_location_translator(location, usage_id=new_usage_id)
        new_usage_id = loc_mapper().add_block_location_translator(location, old_course_id=old_style_course_id)
        other_course_old_style = '{}/{}/{}'.format(org, course, 'delta_run')
        new_usage_id2 = loc_mapper().add_block_location_translator(
            location,
            old_course_id=other_course_old_style,
            usage_id='{}b'.format(new_usage_id)
        )
        with self.assertRaises(DuplicateItemError):
            loc_mapper().add_block_location_translator(location)
Example #50
0
 def mapper(found_id):
     """
     Convert the found id to Location block_id
     """
     locator = BlockUsageLocator.make_relative(from_base_addr, found_id)
     return loc_mapper().translate_locator_to_location(locator).name
Example #51
0
    def translate_location(self,
                           location,
                           published=True,
                           add_entry_if_missing=True,
                           passed_block_id=None):
        """
        Translate the given module location to a Locator.

        The rationale for auto adding entries was that there should be a reasonable default translation
        if the code just trips into this w/o creating translations.

        Will raise ItemNotFoundError if there's no mapping and add_entry_if_missing is False.

        :param location:  a Location pointing to a module
        :param published: a boolean to indicate whether the caller wants the draft or published branch.
        :param add_entry_if_missing: a boolean as to whether to raise ItemNotFoundError or to create an entry if
        the course
        or block is not found in the map.
        :param passed_block_id: what block_id to assign and save if none is found
        (only if add_entry_if_missing)

        NOTE: unlike old mongo, draft branches contain the whole course; so, it applies to all category
        of locations including course.
        """
        course_son = self._interpret_location_course_id(location.course_key)

        cached_value = self._get_locator_from_cache(location, published)
        if cached_value:
            return cached_value

        entry = self.location_map.find_one(course_son)
        if entry is None:
            if add_entry_if_missing:
                # create a new map
                self.create_map_entry(location.course_key)
                entry = self.location_map.find_one(course_son)
            else:
                raise ItemNotFoundError(location)
        else:
            entry = self._migrate_if_necessary([entry])[0]

        block_id = entry['block_map'].get(
            self.encode_key_for_mongo(location.name))
        category = location.category
        if block_id is None:
            if add_entry_if_missing:
                block_id = self._add_to_block_map(location, course_son,
                                                  entry['block_map'],
                                                  passed_block_id)
            else:
                raise ItemNotFoundError(location)
        else:
            # jump_to_id uses a None category.
            if category is None:
                if len(block_id) == 1:
                    # unique match (most common case)
                    category = block_id.keys()[0]
                    block_id = block_id.values()[0]
                else:
                    raise InvalidLocationError()
            elif category in block_id:
                block_id = block_id[category]
            elif add_entry_if_missing:
                block_id = self._add_to_block_map(location, course_son,
                                                  entry['block_map'])
            else:
                raise ItemNotFoundError(location)

        prod_course_locator = CourseLocator(org=entry['org'],
                                            offering=entry['offering'],
                                            branch=entry['prod_branch'])
        published_usage = BlockUsageLocator(prod_course_locator,
                                            block_type=category,
                                            block_id=block_id)
        draft_usage = BlockUsageLocator(prod_course_locator.for_branch(
            entry['draft_branch']),
                                        block_type=category,
                                        block_id=block_id)
        if published:
            result = published_usage
        else:
            result = draft_usage

        self._cache_location_map_entry(location, published_usage, draft_usage)
        return result