コード例 #1
0
ファイル: import_export.py プロジェクト: sliva/edx-platform
def export_output_handler(request, course_key_string):
    """
    Returns the OLX .tar.gz produced by a file export.  Only used in
    environments such as devstack where the output is stored in a local
    filesystem instead of an external service like S3.
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    task_status = _latest_task_status(request, course_key_string,
                                      export_output_handler)
    if task_status and task_status.state == UserTaskStatus.SUCCEEDED:
        artifact = None
        try:
            artifact = UserTaskArtifact.objects.get(status=task_status,
                                                    name='Output')
            tarball = course_import_export_storage.open(artifact.file.name)
            return send_tarball(tarball,
                                artifact.file.storage.size(artifact.file.name))
        except UserTaskArtifact.DoesNotExist:
            raise Http404  # lint-amnesty, pylint: disable=raise-missing-from
        finally:
            if artifact:
                artifact.file.close()
    else:
        raise Http404
コード例 #2
0
ファイル: tasks.py プロジェクト: sekz/edx-platform
def export_olx(self, user_id, course_key_string, language):
    """
    Export a course or library to an OLX .tar.gz archive and prepare it for download.
    """
    set_code_owner_attribute_from_module(__name__)
    courselike_key = CourseKey.from_string(course_key_string)

    try:
        user = User.objects.get(pk=user_id)
    except User.DoesNotExist:
        with translation_language(language):
            self.status.fail(UserErrors.UNKNOWN_USER_ID.format(user_id))
        return
    if not has_course_author_access(user, courselike_key):
        with translation_language(language):
            self.status.fail(UserErrors.PERMISSION_DENIED)
        return

    if isinstance(courselike_key, LibraryLocator):
        courselike_module = modulestore().get_library(courselike_key)
    else:
        courselike_module = modulestore().get_course(courselike_key)

    try:
        self.status.set_state('Exporting')
        tarball = create_export_tarball(courselike_module, courselike_key, {}, self.status)
        artifact = UserTaskArtifact(status=self.status, name='Output')
        artifact.file.save(name=os.path.basename(tarball.name), content=File(tarball))
        artifact.save()
    # catch all exceptions so we can record useful error messages
    except Exception as exception:  # pylint: disable=broad-except
        LOGGER.exception('Error exporting course %s', courselike_key, exc_info=True)
        if self.status.state != UserTaskStatus.FAILED:
            self.status.fail({'raw_error_msg': str(exception)})
        return
コード例 #3
0
ファイル: api.py プロジェクト: rmulder/edx-platform
def check_course_access(course_key, user=None, ip_address=None, url=None):
    """
    Check is the user with this ip_address has access to the given course

    Arguments:
        course_key (CourseKey): Location of the course the user is trying to access.

    Keyword Arguments:
        user (User): The user making the request.  Can be None, in which case
            the user's profile country will not be checked.
        ip_address (str): The IP address of the request.
        url (str): The URL the user is trying to access.  Used in
            log messages.

    Returns:
        Boolean: True if the user has access to the course; False otherwise

    """
    # No-op if the country access feature is not enabled
    if not settings.FEATURES.get('EMBARGO'):
        return True

    # First, check whether there are any restrictions on the course.
    # If not, then we do not need to do any further checks
    course_is_restricted = RestrictedCourse.is_restricted_course(course_key)

    if not course_is_restricted:
        return True

    # Always give global and course staff access, regardless of embargo settings.
    if user is not None and has_course_author_access(user, course_key):
        return True

    if ip_address is not None:
        # Retrieve the country code from the IP address
        # and check it against the allowed countries list for a course
        user_country_from_ip = _country_code_from_ip(ip_address)

        if not CountryAccessRule.check_country_access(course_key,
                                                      user_country_from_ip):
            log.info((u"Blocking user %s from accessing course %s at %s "
                      u"because the user's IP address %s appears to be "
                      u"located in %s."),
                     getattr(user, 'id', '<Not Authenticated>'), course_key,
                     url, ip_address, user_country_from_ip)
            return False

    if user is not None:
        # Retrieve the country code from the user's profile
        # and check it against the allowed countries list for a course.
        user_country_from_profile = _get_user_country_from_profile(user)

        if not CountryAccessRule.check_country_access(
                course_key, user_country_from_profile):
            log.info((u"Blocking user %s from accessing course %s at %s "
                      u"because the user's profile country is %s."), user.id,
                     course_key, url, user_country_from_profile)
            return False

    return True
コード例 #4
0
ファイル: checklists.py プロジェクト: sbxlm/edx-platform
def checklists_handler(request, course_key_string=None):
    '''
    The restful handler for course checklists.
    It allows retrieval of the checklists (as an HTML page).

    GET
        html: return an html page which will show course checklists. Note that only the checklists container
            is returned and that the actual data is determined with a client-side request.
    '''
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    course_module = modulestore().get_course(course_key)

    course_authoring_microfrontend_url = get_proctored_exam_settings_url(course_module)
    proctored_exam_settings_url = (
        '{course_authoring_microfrontend_url}/proctored-exam-settings/{course_key_string}'.format(
            course_authoring_microfrontend_url=course_authoring_microfrontend_url,
            course_key_string=course_key_string,
        )
    )

    return render_to_response('checklists.html', {
        'language_code': request.LANGUAGE_CODE,
        'context_course': course_module,
        'proctored_exam_settings_url': proctored_exam_settings_url,
    })
コード例 #5
0
def export_git(request, course_key_string):
    """
    This method serves up the 'Export to Git' page
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    course_module = modulestore().get_course(course_key)
    failed = False

    log.debug('export_git course_module=%s', course_module)

    msg = ""
    if 'action' in request.GET and course_module.giturl:
        if request.GET['action'] == 'push':
            try:
                git_export_utils.export_to_git(
                    course_module.id,
                    course_module.giturl,
                    request.user,
                )
                msg = _('Course successfully exported to git repository')
            except git_export_utils.GitExportError as ex:
                failed = True
                msg = str(ex)

    return render_to_response('export_git.html', {
        'context_course': course_module,
        'msg': msg,
        'failed': failed,
    })
コード例 #6
0
    def test_notifications_handler_dismiss(self):
        state = CourseRerunUIStateManager.State.FAILED
        should_display = True
        rerun_course_key = CourseLocator(org='testx',
                                         course='test_course',
                                         run='test_run')

        # add an instructor to this course
        user2 = UserFactory()
        add_instructor(rerun_course_key, self.user, user2)

        # create a test notification
        rerun_state = CourseRerunState.objects.update_state(
            course_key=rerun_course_key, new_state=state, allow_not_found=True)
        CourseRerunState.objects.update_should_display(
            entry_id=rerun_state.id, user=user2, should_display=should_display)

        # try to get information on this notification
        notification_dismiss_url = reverse_course_url(
            'course_notifications_handler',
            self.course.id,
            kwargs={
                'action_state_id': rerun_state.id,
            })
        resp = self.client.delete(notification_dismiss_url)
        self.assertEqual(resp.status_code, 200)

        with self.assertRaises(CourseRerunState.DoesNotExist):
            # delete nofications that are dismissed
            CourseRerunState.objects.get(id=rerun_state.id)

        self.assertFalse(has_course_author_access(user2, rerun_course_key))
コード例 #7
0
def tabs_handler(request, course_key_string):
    """
    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).
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    course_item = modulestore().get_course(course_key)

    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:
                return reorder_tabs_handler(course_item, request)
            elif 'tab_id_locator' in request.json:
                return edit_tab_handler(course_item, request)
            else:
                raise NotImplementedError(
                    'Creating or changing tab content is not supported.')

    elif request.method == 'GET':  # assume html
        # get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
        # present in the same order they are displayed in LMS

        tabs_to_render = []
        for tab in CourseTabList.iterate_displayable(course_item,
                                                     user=request.user,
                                                     inline_collections=False,
                                                     include_hidden=True):
            if isinstance(tab, StaticTab):
                # static tab needs its locator information to render itself as an xmodule
                static_tab_loc = course_key.make_usage_key(
                    'static_tab', tab.url_slug)
                tab.locator = static_tab_loc
            tabs_to_render.append(tab)

        return render_to_response(
            'edit-tabs.html', {
                'context_course': course_item,
                'tabs_to_render': tabs_to_render,
                'lms_link': get_lms_link_for_item(course_item.location),
            })
    else:
        return HttpResponseNotFound()
コード例 #8
0
ファイル: tasks.py プロジェクト: sekz/edx-platform
 def user_has_access(user):
     """Return True if user has studio write access to the given course."""
     has_access = has_course_author_access(user, courselike_key)
     if not has_access:
         message = f'User permission denied: {user.username}'
         with translation_language(language):
             self.status.fail(UserErrors.COURSE_PERMISSION_DENIED)
         LOGGER.error(f'{log_prefix}: {message}')
         monitor_import_failure(courselike_key, current_step, message=message)
     return has_access
コード例 #9
0
    def test_rerun_course(self):
        """
        Unit tests for :meth: `contentstore.tasks.rerun_course`
        """
        mongo_course1_id = self.import_and_populate_course()

        # rerun from mongo into split
        split_course3_id = CourseLocator(org="edx3",
                                         course="split3",
                                         run="rerun_test")
        # Mark the action as initiated
        fields = {'display_name': 'rerun'}
        CourseRerunState.objects.initiated(mongo_course1_id, split_course3_id,
                                           self.user, fields['display_name'])
        result = rerun_course.delay(six.text_type(mongo_course1_id),
                                    six.text_type(split_course3_id),
                                    self.user.id,
                                    json.dumps(fields, cls=EdxJSONEncoder))
        self.assertEqual(result.get(), "succeeded")
        self.assertTrue(has_course_author_access(self.user, split_course3_id),
                        "Didn't grant access")
        rerun_state = CourseRerunState.objects.find_first(
            course_key=split_course3_id)
        self.assertEqual(rerun_state.state,
                         CourseRerunUIStateManager.State.SUCCEEDED)

        # try creating rerunning again to same name and ensure it generates error
        result = rerun_course.delay(six.text_type(mongo_course1_id),
                                    six.text_type(split_course3_id),
                                    self.user.id)
        self.assertEqual(result.get(), "duplicate course")
        # the below will raise an exception if the record doesn't exist
        CourseRerunState.objects.find_first(
            course_key=split_course3_id,
            state=CourseRerunUIStateManager.State.FAILED)

        # try to hit the generic exception catch
        with patch('xmodule.modulestore.split_mongo.mongo_connection.MongoConnection.insert_course_index', Mock(side_effect=Exception)):  # lint-amnesty, pylint: disable=line-too-long
            split_course4_id = CourseLocator(org="edx3",
                                             course="split3",
                                             run="rerun_fail")
            fields = {'display_name': 'total failure'}
            CourseRerunState.objects.initiated(split_course3_id,
                                               split_course4_id, self.user,
                                               fields['display_name'])
            result = rerun_course.delay(six.text_type(split_course3_id),
                                        six.text_type(split_course4_id),
                                        self.user.id,
                                        json.dumps(fields, cls=EdxJSONEncoder))
            self.assertIn("exception: ", result.get())
            self.assertIsNone(self.store.get_course(split_course4_id),
                              "Didn't delete course after error")
            CourseRerunState.objects.find_first(
                course_key=split_course4_id,
                state=CourseRerunUIStateManager.State.FAILED)
コード例 #10
0
ファイル: import_export.py プロジェクト: sliva/edx-platform
def export_handler(request, course_key_string):
    """
    The restful handler for exporting a course.

    GET
        html: return html page for import page
        json: not supported
    POST
        Start a Celery task to export the course

    The Studio UI uses a POST request to start the export asynchronously, with
    a link appearing on the page once it's ready.
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    if isinstance(course_key, LibraryLocator):
        courselike_module = modulestore().get_library(course_key)
        context = {
            'context_library':
            courselike_module,
            'courselike_home_url':
            reverse_library_url("library_handler", course_key),
            'library':
            True
        }
    else:
        courselike_module = modulestore().get_course(course_key)
        if courselike_module is None:
            raise Http404
        context = {
            'context_course': courselike_module,
            'courselike_home_url': reverse_course_url("course_handler",
                                                      course_key),
            'library': False
        }
    context['status_url'] = reverse_course_url('export_status_handler',
                                               course_key)

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

    if request.method == 'POST':
        export_olx.delay(request.user.id, course_key_string,
                         request.LANGUAGE_CODE)
        return JsonResponse({'ExportStatus': 1})
    elif 'text/html' in requested_format:
        return render_to_response('export.html', context)
    else:
        # Only HTML request format is supported (no JSON).
        return HttpResponse(status=406)
コード例 #11
0
 def _wrapper_view(self, request, course_id, *args, **kwargs):
     """
     Checks for course author access for the given course by the requesting user.
     Calls the view function if has access, otherwise raises a 403.
     """
     course_key = CourseKey.from_string(course_id)
     if not has_course_author_access(request.user, course_key):
         raise DeveloperErrorViewMixin.api_error(
             status_code=status.HTTP_403_FORBIDDEN,
             developer_message='The requesting user does not have course author permissions.',
             error_code='user_permissions',
         )
     return view(self, request, course_key, *args, **kwargs)
コード例 #12
0
ファイル: tabs.py プロジェクト: cmltaWt0/edx-platform
def tabs_handler(request, course_key_string):
    """
    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).
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    course_item = modulestore().get_course(course_key)

    if "application/json" in request.META.get("HTTP_ACCEPT",
                                              "application/json"):
        if request.method == "GET":  # lint-amnesty, pylint: disable=no-else-raise
            raise NotImplementedError("coming soon")
        else:
            try:
                update_tabs_handler(course_item, request.json, request.user)
            except ValidationError as err:
                return JsonResponseBadRequest(err.detail)
            return JsonResponse()

    elif request.method == "GET":  # assume html
        # get all tabs from the tabs list and select only static tabs (a.k.a. user-created tabs)
        # present in the same order they are displayed in LMS

        tabs_to_render = list(get_course_static_tabs(course_item,
                                                     request.user))

        return render_to_response(
            "edit-tabs.html",
            {
                "context_course": course_item,
                "tabs_to_render": tabs_to_render,
                "lms_link": get_lms_link_for_item(course_item.location),
            },
        )
    else:
        return HttpResponseNotFound()
コード例 #13
0
def _get_item(request, data):
    """
    Obtains from 'data' the locator for an item.
    Next, gets that item from the modulestore (allowing any errors to raise up).
    Finally, verifies that the user has access to the item.

    Returns the item.
    """
    usage_key = UsageKey.from_string(data.get('locator'))
    # This is placed before has_course_author_access() to validate the location,
    # because has_course_author_access() raises  r if location is invalid.
    item = modulestore().get_item(usage_key)

    # use the item's course_key, because the usage_key might not have the run
    if not has_course_author_access(request.user, item.location.course_key):
        raise PermissionDenied()

    return item
コード例 #14
0
def entrance_exam(request, course_key_string):
    """
    The restful handler for entrance exams.
    It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
    deleting assets, and changing the "locked" state of an asset.

    GET
        Retrieves the entrance exam module (metadata) for the specified course
    POST
        Adds an entrance exam module to the specified course.
    DELETE
        Removes the entrance exam from the course
    """
    course_key = CourseKey.from_string(course_key_string)

    # Deny access if the user is valid, but they lack the proper object access privileges
    if not has_course_author_access(request.user, course_key):
        return HttpResponse(status=403)

    # Retrieve the entrance exam module for the specified course (returns 404 if none found)
    if request.method == 'GET':
        return _get_entrance_exam(request, course_key)

    # Create a new entrance exam for the specified course (returns 201 if created)
    elif request.method == 'POST':
        response_format = request.POST.get('format', 'html')
        http_accept = request.META.get('http_accept')
        if response_format == 'json' or 'application/json' in http_accept:
            ee_min_score = request.POST.get('entrance_exam_minimum_score_pct', None)

            # if request contains empty value or none then save the default one.
            entrance_exam_minimum_score_pct = _get_default_entrance_exam_minimum_pct()
            if ee_min_score != '' and ee_min_score is not None:
                entrance_exam_minimum_score_pct = float(ee_min_score)
            return create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct)
        return HttpResponse(status=400)

    # Remove the entrance exam module for the specified course (returns 204 regardless of existence)
    elif request.method == 'DELETE':
        return delete_entrance_exam(request, course_key)

    # No other HTTP verbs/methods are supported at this time
    else:
        return HttpResponse(status=405)
コード例 #15
0
def import_status_handler(request, course_key_string, filename=None):
    """
    Returns an integer corresponding to the status of a file import. These are:

        -X : Import unsuccessful due to some error with X as stage [0-3]
        0 : No status info found (import done or upload still in progress)
        1 : Unpacking
        2 : Verifying
        3 : Updating
        4 : Import successful

    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    # The task status record is authoritative once it's been created
    args = {'course_key_string': course_key_string, 'archive_name': filename}
    name = CourseImportTask.generate_name(args)
    task_status = UserTaskStatus.objects.filter(name=name)
    message = ''
    for status_filter in STATUS_FILTERS:
        task_status = status_filter().filter_queryset(request, task_status,
                                                      import_status_handler)
    task_status = task_status.order_by('-created').first()
    if task_status is None:
        # The task hasn't been initialized yet; did we store info in the session already?
        try:
            session_status = request.session["import_status"]
            status = session_status[course_key_string + filename]
        except KeyError:
            status = 0
    elif task_status.state == UserTaskStatus.SUCCEEDED:
        status = 4
    elif task_status.state in (UserTaskStatus.FAILED, UserTaskStatus.CANCELED):
        status = max(-(task_status.completed_steps + 1), -3)
        artifact = UserTaskArtifact.objects.filter(
            name='Error', status=task_status).order_by('-created').first()
        if artifact:
            message = artifact.text
    else:
        status = min(task_status.completed_steps + 1, 3)

    return JsonResponse({"ImportStatus": status, "Message": message})
コード例 #16
0
ファイル: import_export.py プロジェクト: sliva/edx-platform
def import_handler(request, course_key_string):
    """
    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
    """
    courselike_key = CourseKey.from_string(course_key_string)
    library = isinstance(courselike_key, LibraryLocator)
    if library:
        successful_url = reverse_library_url('library_handler', courselike_key)
        context_name = 'context_library'
        courselike_module = modulestore().get_library(courselike_key)
    else:
        successful_url = reverse_course_url('course_handler', courselike_key)
        context_name = 'context_course'
        courselike_module = modulestore().get_course(courselike_key)
    if not has_course_author_access(request.user, courselike_key):
        raise PermissionDenied()

    if 'application/json' in request.META.get('HTTP_ACCEPT',
                                              'application/json'):
        if request.method == 'GET':  # lint-amnesty, pylint: disable=no-else-raise
            raise NotImplementedError('coming soon')
        else:
            return _write_chunk(request, courselike_key)
    elif request.method == 'GET':  # assume html
        status_url = reverse_course_url("import_status_handler",
                                        courselike_key,
                                        kwargs={'filename': "fillerName"})
        return render_to_response(
            'import.html', {
                context_name: courselike_module,
                'successful_import_redirect_url': successful_url,
                'import_status_url': status_url,
                'library': isinstance(courselike_key, LibraryLocator)
            })
    else:
        return HttpResponseNotFound()
コード例 #17
0
def _get_item_in_course(request, usage_key):
    """
    Helper method for getting the old location, containing course,
    item, lms_link, and preview_lms_link for a given locator.

    Verifies that the caller has permission to access this item.
    """
    # usage_key's course_key may have an empty run property
    usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))

    course_key = usage_key.course_key

    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    course = modulestore().get_course(course_key)
    item = modulestore().get_item(usage_key, depth=1)
    lms_link = get_lms_link_for_item(item.location)
    preview_lms_link = get_lms_link_for_item(item.location, preview=True)

    return course, item, lms_link, preview_lms_link
コード例 #18
0
def assets_handler(request, course_key_string=None, asset_key_string=None):
    '''
    The restful handler for assets.
    It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
    deleting assets, and changing the 'locked' state of an asset.

    GET
        html: return an html page which will show all course assets. Note that only the asset container
            is returned and that the actual assets are filled in with a client-side request.
        json: returns a page of assets. The following parameters are supported:
            page: the desired page of results (defaults to 0)
            page_size: the number of items per page (defaults to 50)
            sort: the asset field to sort by (defaults to 'date_added')
            direction: the sort direction (defaults to 'descending')
            asset_type: the file type to filter items to (defaults to All)
            text_search: string to filter results by file name (defaults to '')
    POST
        json: create (or update?) an asset. The only updating that can be done is changing the lock state.
    PUT
        json: update the locked state of an asset
    DELETE
        json: delete an asset
    '''
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    response_format = _get_response_format(request)
    if _request_response_format_is_json(request, response_format):
        if request.method == 'GET':
            return _assets_json(request, course_key)

        asset_key = AssetKey.from_string(
            asset_key_string) if asset_key_string else None
        return _update_asset(request, course_key, asset_key)

    elif request.method == 'GET':  # assume html
        return _asset_index(request, course_key)

    return HttpResponseNotFound()
コード例 #19
0
ファイル: tasks.py プロジェクト: ririfat750/edx-platform
def import_olx(self, user_id, course_key_string, archive_path, archive_name,
               language):
    """
    Import a course or library from a provided OLX .tar.gz archive.
    """
    set_code_owner_attribute_from_module(__name__)
    courselike_key = CourseKey.from_string(course_key_string)
    try:
        user = User.objects.get(pk=user_id)
    except User.DoesNotExist:
        with translation_language(language):
            self.status.fail(_(u'Unknown User ID: {0}').format(user_id))
        return
    if not has_course_author_access(user, courselike_key):
        with translation_language(language):
            self.status.fail(_(u'Permission denied'))
        return

    is_library = isinstance(courselike_key, LibraryLocator)
    is_course = not is_library
    if is_library:
        root_name = LIBRARY_ROOT
        courselike_module = modulestore().get_library(courselike_key)
        import_func = import_library_from_xml
    else:
        root_name = COURSE_ROOT
        courselike_module = modulestore().get_course(courselike_key)
        import_func = import_course_from_xml

    # Locate the uploaded OLX archive (and download it from S3 if necessary)
    # Do everything in a try-except block to make sure everything is properly cleaned up.
    data_root = path(settings.GITHUB_REPO_ROOT)
    subdir = base64.urlsafe_b64encode(
        repr(courselike_key).encode('utf-8')).decode('utf-8')
    course_dir = data_root / subdir
    try:
        self.status.set_state(u'Unpacking')

        if not archive_name.endswith(u'.tar.gz'):
            with translation_language(language):
                self.status.fail(
                    _(u'We only support uploading a .tar.gz file.'))
                return

        temp_filepath = course_dir / get_valid_filename(archive_name)
        if not course_dir.isdir():
            os.mkdir(course_dir)

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

        # Copy the OLX archive from where it was uploaded to (S3, Swift, file system, etc.)
        if not course_import_export_storage.exists(archive_path):
            LOGGER.info(u'Course import %s: Uploaded file %s not found',
                        courselike_key, archive_path)
            with translation_language(language):
                self.status.fail(_(u'Tar file not found'))
            return
        with course_import_export_storage.open(archive_path, 'rb') as source:
            with open(temp_filepath, 'wb') as destination:

                def read_chunk():
                    """
                    Read and return a sequence of bytes from the source file.
                    """
                    return source.read(FILE_READ_CHUNK)

                for chunk in iter(read_chunk, b''):
                    destination.write(chunk)
        LOGGER.info(u'Course import %s: Download from storage complete',
                    courselike_key)
        # Delete from source location
        course_import_export_storage.delete(archive_path)

        # If the course has an entrance exam then remove it and its corresponding milestone.
        # current course state before import.
        if is_course:
            if courselike_module.entrance_exam_enabled:
                fake_request = RequestFactory().get(u'/')
                fake_request.user = user
                from .views.entrance_exam import remove_entrance_exam_milestone_reference
                # TODO: Is this really ok?  Seems dangerous for a live course
                remove_entrance_exam_milestone_reference(
                    fake_request, courselike_key)
                LOGGER.info(
                    u'entrance exam milestone content reference for course %s has been removed',
                    courselike_module.id)
    # Send errors to client with stage at which error occurred.
    except Exception as exception:  # pylint: disable=broad-except
        if course_dir.isdir():
            shutil.rmtree(course_dir)
            LOGGER.info(u'Course import %s: Temp data cleared', courselike_key)

        LOGGER.exception(u'Error importing course %s',
                         courselike_key,
                         exc_info=True)
        self.status.fail(text_type(exception))
        return

    # try-finally block for proper clean up after receiving file.
    try:
        tar_file = tarfile.open(temp_filepath)
        try:
            safetar_extractall(tar_file, (course_dir + u'/'))
        except SuspiciousOperation as exc:
            LOGGER.info(u'Course import %s: Unsafe tar file - %s',
                        courselike_key, exc.args[0])
            with translation_language(language):
                self.status.fail(_(u'Unsafe tar file. Aborting import.'))
            return
        finally:
            tar_file.close()

        LOGGER.info(u'Course import %s: Uploaded file extracted',
                    courselike_key)
        self.status.set_state(u'Verifying')
        self.status.increment_completed_steps()

        # 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 directory_path, _dirnames, filenames in os.walk(directory):
                for filename in filenames:
                    yield (filename, directory_path)

        def get_dir_for_filename(directory, filename):
            """
            Returns the directory path 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 name, directory_path in get_all_files(directory):
                if name == filename:
                    return directory_path
            return None

        dirpath = get_dir_for_filename(course_dir, root_name)
        if not dirpath:
            with translation_language(language):
                self.status.fail(
                    _(u'Could not find the {0} file in the package.').format(
                        root_name))
                return

        dirpath = os.path.relpath(dirpath, data_root)
        LOGGER.debug(u'found %s at %s', root_name, dirpath)

        LOGGER.info(u'Course import %s: Extracted file verified',
                    courselike_key)
        self.status.set_state(u'Updating')
        self.status.increment_completed_steps()

        courselike_items = import_func(modulestore(),
                                       user.id,
                                       settings.GITHUB_REPO_ROOT, [dirpath],
                                       load_error_modules=False,
                                       static_content_store=contentstore(),
                                       target_id=courselike_key)

        new_location = courselike_items[0].location
        LOGGER.debug(u'new course at %s', new_location)

        LOGGER.info(u'Course import %s: Course import successful',
                    courselike_key)
    except Exception as exception:  # pylint: disable=broad-except
        LOGGER.exception(u'error importing course', exc_info=True)
        self.status.fail(text_type(exception))
    finally:
        if course_dir.isdir():
            shutil.rmtree(course_dir)
            LOGGER.info(u'Course import %s: Temp data cleared', courselike_key)

        if self.status.state == u'Updating' and is_course:
            # Reload the course so we have the latest state
            course = modulestore().get_course(courselike_key)
            if course.entrance_exam_enabled:
                entrance_exam_chapter = modulestore().get_items(
                    course.id,
                    qualifiers={u'category': u'chapter'},
                    settings={u'is_entrance_exam': True})[0]

                metadata = {
                    u'entrance_exam_id':
                    text_type(entrance_exam_chapter.location)
                }
                CourseMetadata.update_from_dict(metadata, course, user)
                from .views.entrance_exam import add_entrance_exam_milestone
                add_entrance_exam_milestone(course.id, entrance_exam_chapter)
                LOGGER.info(u'Course %s Entrance exam imported', course.id)
コード例 #20
0
def export_status_handler(request, course_key_string):
    """
    Returns an integer corresponding to the status of a file export. These are:

        -X : Export unsuccessful due to some error with X as stage [0-3]
        0 : No status info found (export done or task not yet created)
        1 : Exporting
        2 : Compressing
        3 : Export successful

    If the export was successful, a URL for the generated .tar.gz file is also
    returned.
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    # The task status record is authoritative once it's been created
    task_status = _latest_task_status(request, course_key_string, export_status_handler)
    output_url = None
    error = None
    if task_status is None:
        # The task hasn't been initialized yet; did we store info in the session already?
        try:
            session_status = request.session["export_status"]
            status = session_status[course_key_string]
        except KeyError:
            status = 0
    elif task_status.state == UserTaskStatus.SUCCEEDED:
        status = 3
        artifact = UserTaskArtifact.objects.get(status=task_status, name='Output')
        if isinstance(artifact.file.storage, FileSystemStorage):
            output_url = reverse_course_url('export_output_handler', course_key)
        elif isinstance(artifact.file.storage, S3BotoStorage):
            filename = os.path.basename(artifact.file.name)
            disposition = f'attachment; filename="{filename}"'
            output_url = artifact.file.storage.url(artifact.file.name, response_headers={
                'response-content-disposition': disposition,
                'response-content-encoding': 'application/octet-stream',
                'response-content-type': 'application/x-tgz'
            })
        elif isinstance(artifact.file.storage, S3Boto3Storage):
            filename = os.path.basename(artifact.file.name)
            disposition = f'attachment; filename="{filename}"'
            output_url = artifact.file.storage.url(artifact.file.name, parameters={
                'ResponseContentDisposition': disposition,
                'ResponseContentEncoding': 'application/octet-stream',
                'ResponseContentType': 'application/x-tgz'
            })
        else:
            output_url = artifact.file.storage.url(artifact.file.name)
    elif task_status.state in (UserTaskStatus.FAILED, UserTaskStatus.CANCELED):
        status = max(-(task_status.completed_steps + 1), -2)
        errors = UserTaskArtifact.objects.filter(status=task_status, name='Error')
        if errors:
            error = errors[0].text
            try:
                error = json.loads(error)
            except ValueError:
                # Wasn't JSON, just use the value as a string
                pass
    else:
        status = min(task_status.completed_steps + 1, 2)

    response = {"ExportStatus": status}
    if output_url:
        response['ExportOutput'] = output_url
    elif error:
        response['ExportError'] = error
    return JsonResponse(response)
コード例 #21
0
ファイル: views.py プロジェクト: edx/edx-platform
def cohort_handler(request, course_key_string, cohort_id=None):
    """
    The restful handler for cohort requests. Requires JSON.
    GET
        If a cohort ID is specified, returns a JSON representation of the cohort
            (name, id, user_count, assignment_type, user_partition_id, group_id).
        If no cohort ID is specified, returns the JSON representation of all cohorts.
           This is returned as a dict with the list of cohort information stored under the
           key `cohorts`.
    PUT or POST or PATCH
        If a cohort ID is specified, updates the cohort with the specified ID. Currently the only
        properties that can be updated are `name`, `user_partition_id` and `group_id`.
        Returns the JSON representation of the updated cohort.
        If no cohort ID is specified, creates a new cohort and returns the JSON representation of the updated
        cohort.
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise Http404(
            'The requesting user does not have course author permissions.')

    course = get_course(course_key)

    if request.method == 'GET':
        if not cohort_id:
            all_cohorts = [
                _get_cohort_representation(c, course)
                for c in cohorts.get_course_cohorts(course)
            ]
            return JsonResponse({'cohorts': all_cohorts})
        else:
            cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
            return JsonResponse(_get_cohort_representation(cohort, course))
    else:
        name = request.json.get('name')
        assignment_type = request.json.get('assignment_type')
        if not name:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse({"error": "Cohort name must be specified."},
                                400)
        if not assignment_type:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse(
                {"error": "Assignment type must be specified."}, 400)
        # If cohort_id is specified, update the existing cohort. Otherwise, create a new cohort.
        if cohort_id:
            cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
            if name != cohort.name:
                if cohorts.is_cohort_exists(course_key, name):
                    err_msg = gettext(
                        "A cohort with the same name already exists.")
                    return JsonResponse({"error": str(err_msg)}, 400)
                cohort.name = name
                cohort.save()
            try:
                cohorts.set_assignment_type(cohort, assignment_type)
            except ValueError as err:
                return JsonResponse({"error": str(err)}, 400)
        else:
            try:
                cohort = cohorts.add_cohort(course_key, name, assignment_type)
            except ValueError as err:
                return JsonResponse({"error": str(err)}, 400)

        group_id = request.json.get('group_id')
        if group_id is not None:
            user_partition_id = request.json.get('user_partition_id')
            if user_partition_id is None:
                # Note: error message not translated because it is not exposed to the user (UI prevents this state).
                return JsonResponse(
                    {
                        "error":
                        "If group_id is specified, user_partition_id must also be specified."
                    }, 400)
            existing_group_id, existing_partition_id = cohorts.get_group_info_for_cohort(
                cohort)
            if group_id != existing_group_id or user_partition_id != existing_partition_id:
                unlink_cohort_partition_group(cohort)
                link_cohort_to_partition_group(cohort, user_partition_id,
                                               group_id)
        else:
            # If group_id was specified as None, unlink the cohort if it previously was associated with a group.
            existing_group_id, _ = cohorts.get_group_info_for_cohort(cohort)
            if existing_group_id is not None:
                unlink_cohort_partition_group(cohort)

        return JsonResponse(_get_cohort_representation(cohort, course))
コード例 #22
0
ファイル: api.py プロジェクト: uetuluk/edx-platform
def check_course_access(
        course_key: CourseKey,
        user: Optional[types.User] = None,
        ip_addresses: Optional[List[str]] = None,
        url: Optional[str] = None,
) -> bool:
    """
    Check is the user with this ip_addresses chain has access to the given course

    Arguments:
        course_key: Location of the course the user is trying to access.
        user: The user making the request. Can be None, in which case the user's profile country will not be checked.
        ip_addresses: The full external chain of IP addresses of the request.
        url: The URL the user is trying to access. Used in log messages.

    Returns:
        True if the user has access to the course; False otherwise

    """
    # No-op if the country access feature is not enabled
    if not settings.FEATURES.get('EMBARGO'):
        return True

    # First, check whether there are any restrictions on the course.
    # If not, then we do not need to do any further checks
    course_is_restricted = RestrictedCourse.is_restricted_course(course_key)

    if not course_is_restricted:
        return True

    # Always give global and course staff access, regardless of embargo settings.
    if user is not None and has_course_author_access(user, course_key):
        return True

    if ip_addresses is not None:
        # Check every IP address provided and deny access if ANY of them fail our country checks
        for ip_address in ip_addresses:
            # Retrieve the country code from the IP address
            # and check it against the allowed countries list for a course
            user_country_from_ip = country_code_from_ip(ip_address)

            if not CountryAccessRule.check_country_access(course_key, user_country_from_ip):
                log.info(
                    (
                        "Blocking user %s from accessing course %s at %s "
                        "because the user's IP address %s appears to be "
                        "located in %s."
                    ),
                    getattr(user, 'id', '<Not Authenticated>'),
                    course_key,
                    url,
                    ip_address,
                    user_country_from_ip
                )
                return False

    if user is not None:
        # Retrieve the country code from the user's profile
        # and check it against the allowed countries list for a course.
        user_country_from_profile = _get_user_country_from_profile(user)

        if not CountryAccessRule.check_country_access(course_key, user_country_from_profile):
            log.info(
                (
                    "Blocking user %s from accessing course %s at %s "
                    "because the user's profile country is %s."
                ),
                user.id, course_key, url, user_country_from_profile
            )
            return False

    return True
コード例 #23
0
    def test_get_all_users(self):
        """
        Test getting all authors for a course where their permissions run the gamut of allowed group
        types.
        """
        # first check the course creator.has explicit access (don't use has_access as is_staff
        # will trump the actual test)
        self.assertTrue(
            CourseInstructorRole(self.course_key).has_user(self.user),
            "Didn't add creator as instructor.")
        users = copy.copy(self.users)
        # doesn't use role.users_with_role b/c it's verifying the roles.py behavior
        user_by_role = {}
        # add the misc users to the course in different groups
        for role in [
                CourseInstructorRole, CourseStaffRole, OrgStaffRole,
                OrgInstructorRole
        ]:
            user_by_role[role] = []
            # Org-based roles are created via org name, rather than course_key
            if (role is OrgStaffRole) or (role is OrgInstructorRole):
                group = role(self.course_key.org)
            else:
                group = role(self.course_key)
            # NOTE: this loop breaks the roles.py abstraction by purposely assigning
            # users to one of each possible groupname in order to test that has_course_author_access
            # and remove_user work
            user = users.pop()
            group.add_users(user)
            user_by_role[role].append(user)
            self.assertTrue(auth.has_course_author_access(user, self.course_key), f"{user} does not have access")  # lint-amnesty, pylint: disable=line-too-long

        course_team_url = reverse_course_url('course_team_handler',
                                             self.course_key)
        response = self.client.get_html(course_team_url)
        for role in [CourseInstructorRole, CourseStaffRole
                     ]:  # Global and org-based roles don't appear on this page
            for user in user_by_role[role]:
                self.assertContains(response, user.email)

        # test copying course permissions
        copy_course_key = self.store.make_course_key('copyu',
                                                     'copydept.mycourse',
                                                     'myrun')
        for role in [
                CourseInstructorRole, CourseStaffRole, OrgStaffRole,
                OrgInstructorRole
        ]:
            if (role is OrgStaffRole) or (role is OrgInstructorRole):
                auth.add_users(self.user, role(copy_course_key.org),
                               *role(self.course_key.org).users_with_role())
            else:
                auth.add_users(self.user, role(copy_course_key),
                               *role(self.course_key).users_with_role())
        # verify access in copy course and verify that removal from source course w/ the various
        # groupnames works
        for role in [
                CourseInstructorRole, CourseStaffRole, OrgStaffRole,
                OrgInstructorRole
        ]:
            for user in user_by_role[role]:
                # forcefully decache the groups: premise is that any real request will not have
                # multiple objects repr the same user but this test somehow uses different instance
                # in above add_users call
                if hasattr(user, '_roles'):
                    del user._roles

                self.assertTrue(
                    auth.has_course_author_access(user, copy_course_key),
                    f"{user} no copy access")
                if (role is OrgStaffRole) or (role is OrgInstructorRole):
                    auth.remove_users(self.user, role(self.course_key.org),
                                      user)
                else:
                    auth.remove_users(self.user, role(self.course_key), user)
                self.assertFalse(auth.has_course_author_access(user, self.course_key), f"{user} remove didn't work")  # lint-amnesty, pylint: disable=line-too-long
コード例 #24
0
    def get(self, request, subsection_id):
        """
        Returns subection grade data, override grade data and a history of changes made to
        a specific users specific subsection grade.

        Args:
            subsection_id: String representation of a usage_key, which is an opaque key of
            a persistant subection grade.
            user_id: An integer represenation of a user

        """
        try:
            usage_key = UsageKey.from_string(subsection_id)
        except InvalidKeyError:
            raise self.api_error(status_code=status.HTTP_404_NOT_FOUND,
                                 developer_message='Invalid UsageKey',
                                 error_code='invalid_usage_key')

        if not has_course_author_access(request.user, usage_key.course_key):
            raise DeveloperErrorViewMixin.api_error(
                status_code=status.HTTP_403_FORBIDDEN,
                developer_message=
                'The requesting user does not have course author permissions.',
                error_code='user_permissions',
            )

        try:
            user_id = int(request.GET.get('user_id'))
        except ValueError:
            raise self.api_error(status_code=status.HTTP_404_NOT_FOUND,
                                 developer_message='Invalid UserID',
                                 error_code='invalid_user_id')
        success = True
        err_msg = ""
        override = None
        history = []
        history_record_limit = request.GET.get('history_record_limit')
        if history_record_limit is not None:
            try:
                history_record_limit = int(history_record_limit)
            except ValueError:
                history_record_limit = 0

        try:
            original_grade = PersistentSubsectionGrade.read_grade(
                user_id, usage_key)
            if original_grade is not None and hasattr(original_grade,
                                                      'override'):
                override = original_grade.override
                # pylint: disable=no-member
                history = list(
                    PersistentSubsectionGradeOverride.history.filter(
                        grade_id=original_grade.id).order_by('history_date')
                    [:history_record_limit])
            grade_data = {
                'earned_all': original_grade.earned_all,
                'possible_all': original_grade.possible_all,
                'earned_graded': original_grade.earned_graded,
                'possible_graded': original_grade.possible_graded,
            }
        except PersistentSubsectionGrade.DoesNotExist:
            try:
                grade_data = self._get_grade_data_for_not_attempted_assignment(
                    user_id, usage_key)
            except SubsectionUnavailableToUserException as exc:
                success = False
                err_msg = str(exc)
                grade_data = {
                    'earned_all': 0,
                    'possible_all': 0,
                    'earned_graded': 0,
                    'possible_graded': 0,
                }

        response_data = {
            'success': success,
            'original_grade': grade_data,
            'override': override,
            'history': history,
            'subsection_id': usage_key,
            'user_id': user_id,
            'course_id': usage_key.course_key,
        }
        if not success:
            response_data['error_message'] = err_msg
        results = SubsectionGradeResponseSerializer(response_data)
        return Response(results.data)