Beispiel #1
0
def xblock_view_handler(request, usage_key_string, view_name):
    """
    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
    """
    usage_key = usage_key_with_run(usage_key_string)
    if not has_course_access(request.user, usage_key.course_key):
        raise PermissionDenied()

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

    if 'application/json' in accept_header:
        store = modulestore()
        xblock = store.get_item(usage_key)
        container_views = [
            'container_preview', 'reorderable_container_child_preview'
        ]

        # wrap the generated fragment in the xmodule_editor div so that the javascript
        # can bind to it correctly
        xblock.runtime.wrappers.append(
            partial(
                wrap_xblock,
                'StudioRuntime',
                usage_id_serializer=unicode,
                request_token=request_token(request),
            ))

        if view_name == STUDIO_VIEW:
            try:
                fragment = xblock.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",
                          xblock,
                          exc_info=True)
                fragment = Fragment(
                    render_to_string('html_error.html', {'message': str(exc)}))

            store.update_item(xblock, request.user.id)
        elif view_name in (PREVIEW_VIEWS + container_views):
            is_pages_view = view_name == STUDENT_VIEW  # Only the "Pages" view uses student view in Studio

            # Determine the items to be shown as reorderable. Note that the view
            # 'reorderable_container_child_preview' is only rendered for xblocks that
            # are being shown in a reorderable container, so the xblock is automatically
            # added to the list.
            reorderable_items = set()
            if view_name == 'reorderable_container_child_preview':
                reorderable_items.add(xblock.location)

            # Set up the context to be passed to each XBlock's render method.
            context = {
                'is_pages_view':
                is_pages_view,  # This setting disables the recursive wrapping of xblocks
                'is_unit_page': is_unit(xblock),
                'root_xblock': xblock if
                (view_name == 'container_preview') else None,
                'reorderable_items': reorderable_items
            }

            fragment = get_preview_fragment(request, xblock, context)

            # Note that the container view recursively adds headers into the preview fragment,
            # so only the "Pages" view requires that this extra wrapper be included.
            if is_pages_view:
                fragment.content = render_to_string(
                    'component.html', {
                        'xblock_context':
                        context,
                        'xblock':
                        xblock,
                        'locator':
                        usage_key,
                        'preview':
                        fragment.content,
                        'label':
                        xblock.display_name or xblock.scope_ids.block_type,
                    })
        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)
Beispiel #2
0
def _create_item(request):
    """View for create items."""
    usage_key = usage_key_with_run(request.json['parent_locator'])
    category = request.json['category']

    display_name = request.json.get('display_name')

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

    store = modulestore()
    parent = store.get_item(usage_key)
    dest_usage_key = usage_key.replace(category=category, name=uuid4().hex)

    # get the metadata, display_name, and definition from the request
    metadata = {}
    data = None
    template_id = request.json.get('boilerplate')
    if template_id:
        clz = parent.runtime.load_block_type(category)
        if clz is not None:
            template = clz.get_template(template_id)
            if template is not None:
                metadata = template.get('metadata', {})
                data = template.get('data')

    if display_name is not None:
        metadata['display_name'] = display_name

    # TODO need to fix components that are sending definition_data as strings, instead of as dicts
    # For now, migrate them into dicts here.
    if isinstance(data, basestring):
        data = {'data': data}

    created_block = store.create_child(
        request.user.id,
        usage_key,
        dest_usage_key.block_type,
        block_id=dest_usage_key.block_id,
        definition_data=data,
        metadata=metadata,
        runtime=parent.runtime,
    )

    # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
    # if we add one then we need to also add it to the policy information (i.e. metadata)
    # we should remove this once we can break this reference from the course to static tabs
    if category == 'static_tab':
        display_name = display_name or _("Empty")  # Prevent name being None
        course = store.get_course(dest_usage_key.course_key)
        course.tabs.append(
            StaticTab(
                name=display_name,
                url_slug=dest_usage_key.name,
            ))
        store.update_item(course, request.user.id)

    return JsonResponse({
        "locator": unicode(created_block.location),
        "courseKey": unicode(created_block.location.course_key)
    })
Beispiel #3
0
def _upload_asset(request, location):
    '''
    This method allows for POST uploading of files into the course asset
    library, which will be supported by GridFS in MongoDB.
    '''
    old_location = loc_mapper().translate_locator_to_location(location)
    # Does the course actually exist?!? Get anything from it to prove its
    # existence
    try:
        modulestore().get_item(old_location)
    except:
        # no return it as a Bad Request response
        logging.error("Could not find course: %s", old_location)
        return HttpResponseBadRequest()

    # get all filename
    course_reference = StaticContent.compute_location(old_location.org,
                                                      old_location.course,
                                                      old_location.name)

    filename_arr = [
        up_f["displayname"]
        for up_f in contentstore().get_all_content_for_course(
            course_reference, start=0, maxresults=-1, sort=None)[0]
    ] or []

    # compute a 'filename' which is similar to the location formatting, we're
    # using the 'filename' nomenclature since we're using a FileSystem paradigm
    # here. We're just imposing the Location string formatting expectations to
    # keep things a bit more consistent
    upload_file = request.FILES['file']
    filename = upload_file.name

    def acquire_purename_and_suffix(f_n):
        fn_sp = f_n.split(".")
        return ('.'.join(fn_sp[0:-1]), fn_sp[-1])

    if filename in filename_arr:
        # filter same suffix filename
        pure_filename, file_suffix = acquire_purename_and_suffix(filename)

        pattern_str = "(" + pure_filename.replace("(", "\(").replace(
            ")", "\)") + ")(" + "\()(\d+)(\))"
        pattern = re.compile(pattern_str)

        start_copy = 0
        for f in filename_arr:
            f_n, f_s = acquire_purename_and_suffix(f)

            if f_s != file_suffix:
                continue

            match_obj = pattern.search(f_n)

            if match_obj:
                start_copy = int(match_obj.groups()[2]) if int(
                    match_obj.groups()[2]) > start_copy else start_copy

        filename = pure_filename + "(" + str(start_copy +
                                             1) + ")." + file_suffix

    mime_type = upload_file.content_type

    content_loc = StaticContent.compute_location(old_location.org,
                                                 old_location.course, filename)

    chunked = upload_file.multiple_chunks()
    sc_partial = partial(StaticContent, content_loc, filename, mime_type)
    if chunked:
        content = sc_partial(upload_file.chunks())
        tempfile_path = upload_file.temporary_file_path()
    else:
        content = sc_partial(upload_file.read())
        tempfile_path = None

    # first let's see if a thumbnail can be created
    (thumbnail_content,
     thumbnail_location) = contentstore().generate_thumbnail(
         content, tempfile_path=tempfile_path)

    # delete cached thumbnail even if one couldn't be created this time (else
    # the old thumbnail will continue to show)
    del_cached_content(thumbnail_location)
    # now store thumbnail location only if we could create it
    if thumbnail_content is not None:
        content.thumbnail_location = thumbnail_location

    # then commit the content
    contentstore().save(content)
    del_cached_content(content.location)

    # readback the saved content - we need the database timestamp
    readback = contentstore().find(content.location)

    locked = getattr(content, 'locked', False)
    response_payload = {
        'asset':
        _get_asset_json(content.name, readback.last_modified_at,
                        content.location, content.thumbnail_location, locked),
        'msg':
        _('Upload completed')
    }

    return JsonResponse(response_payload)
Beispiel #4
0
def check_transcripts(request):
    """
    Check state of transcripts availability.

    request.GET['data'] has key `videos`, which can contain any of the following::

        [
            {u'type': u'youtube', u'video': u'OEoXaMPEzfM', u'mode': u'youtube'},
            {u'type': u'html5',    u'video': u'video1',             u'mode': u'mp4'}
            {u'type': u'html5',    u'video': u'video2',             u'mode': u'webm'}
        ]
        `type` is youtube or html5
        `video` is html5 or youtube video_id
        `mode` is youtube, ,p4 or webm

    Returns transcripts_presence dict::

        html5_local: list of html5 ids, if subtitles exist locally for them;
        is_youtube_mode: bool, if we have youtube_id, and as youtube mode is of higher priority, reflect this with flag;
        youtube_local: bool, if youtube transcripts exist locally;
        youtube_server: bool, if youtube transcripts exist on server;
        youtube_diff: bool, if youtube transcripts exist on youtube server, and are different from local youtube ones;
        current_item_subs: string, value of item.sub field;
        status: string, 'Error' or 'Success';
        subs: string, new value of item.sub field, that should be set in module;
        command: string, action to front-end what to do and what to show to user.
    """
    transcripts_presence = {
        'html5_local': [],
        'html5_equal': False,
        'is_youtube_mode': False,
        'youtube_local': False,
        'youtube_server': False,
        'youtube_diff': True,
        'current_item_subs': None,
        'status': 'Error',
    }
    try:
        __, videos, item = _validate_transcripts_data(request)
    except TranscriptsRequestValidationException as e:
        return error_response(transcripts_presence, e.message)

    transcripts_presence['status'] = 'Success'

    filename = 'subs_{0}.srt.sjson'.format(item.sub)
    content_location = StaticContent.compute_location(item.location.org,
                                                      item.location.course,
                                                      filename)
    try:
        local_transcripts = contentstore().find(content_location).data
        transcripts_presence['current_item_subs'] = item.sub
    except NotFoundError:
        pass

    # Check for youtube transcripts presence
    youtube_id = videos.get('youtube', None)
    if youtube_id:
        transcripts_presence['is_youtube_mode'] = True

        # youtube local
        filename = 'subs_{0}.srt.sjson'.format(youtube_id)
        content_location = StaticContent.compute_location(
            item.location.org, item.location.course, filename)
        try:
            local_transcripts = contentstore().find(content_location).data
            transcripts_presence['youtube_local'] = True
        except NotFoundError:
            log.debug("Can't find transcripts in storage for youtube id: %s",
                      youtube_id)

        # youtube server
        youtube_api = copy.deepcopy(settings.YOUTUBE_API)
        youtube_api['params']['v'] = youtube_id
        youtube_response = requests.get(youtube_api['url'],
                                        params=youtube_api['params'])

        if youtube_response.status_code == 200 and youtube_response.text:
            transcripts_presence['youtube_server'] = True
        #check youtube local and server transcripts for equality
        if transcripts_presence['youtube_server'] and transcripts_presence[
                'youtube_local']:
            try:
                youtube_server_subs = get_transcripts_from_youtube(youtube_id)
                if json.loads(
                        local_transcripts
                ) == youtube_server_subs:  # check transcripts for equality
                    transcripts_presence['youtube_diff'] = False
            except GetTranscriptsFromYouTubeException:
                pass

    # Check for html5 local transcripts presence
    html5_subs = []
    for html5_id in videos['html5']:
        filename = 'subs_{0}.srt.sjson'.format(html5_id)
        content_location = StaticContent.compute_location(
            item.location.org, item.location.course, filename)
        try:
            html5_subs.append(contentstore().find(content_location).data)
            transcripts_presence['html5_local'].append(html5_id)
        except NotFoundError:
            log.debug(
                "Can't find transcripts in storage for non-youtube video_id: %s",
                html5_id)
        if len(html5_subs) == 2:  # check html5 transcripts for equality
            transcripts_presence['html5_equal'] = json.loads(
                html5_subs[0]) == json.loads(html5_subs[1])

    command, subs_to_use = _transcripts_logic(transcripts_presence, videos)
    transcripts_presence.update({
        'command': command,
        'subs': subs_to_use,
    })
    return JsonResponse(transcripts_presence)
 def test_get_sailthru_list_map_error(self, mock_sailthru_client):
     """Test when error occurred while fetching data from sailthru"""
     mock_sailthru_client.api_get.return_value = SailthruResponse(
         JsonResponse({'error': 43, 'errormsg': 'Got an error'})
     )
     self.assertEqual(_get_list_from_email_marketing_provider(mock_sailthru_client), {})
Beispiel #6
0
def discussion_topics(request, course_key_string):
    """
    The handler for divided discussion categories requests.
    This will raise 404 if user is not staff.

    Returns the JSON representation of discussion topics w.r.t categories for the course.

    Example:
        >>> example = {
        >>>               "course_wide_discussions": {
        >>>                   "entries": {
        >>>                       "General": {
        >>>                           "sort_key": "General",
        >>>                           "is_divided": True,
        >>>                           "id": "i4x-edx-eiorguegnru-course-foobarbaz"
        >>>                       }
        >>>                   }
        >>>                   "children": ["General", "entry"]
        >>>               },
        >>>               "inline_discussions" : {
        >>>                   "subcategories": {
        >>>                       "Getting Started": {
        >>>                           "subcategories": {},
        >>>                           "children": [
        >>>                               ["Working with Videos", "entry"],
        >>>                               ["Videos on edX", "entry"]
        >>>                           ],
        >>>                           "entries": {
        >>>                               "Working with Videos": {
        >>>                                   "sort_key": None,
        >>>                                   "is_divided": False,
        >>>                                   "id": "d9f970a42067413cbb633f81cfb12604"
        >>>                               },
        >>>                               "Videos on edX": {
        >>>                                   "sort_key": None,
        >>>                                   "is_divided": False,
        >>>                                   "id": "98d8feb5971041a085512ae22b398613"
        >>>                               }
        >>>                           }
        >>>                       },
        >>>                       "children": ["Getting Started", "subcategory"]
        >>>                   },
        >>>               }
        >>>          }
    """
    course_key = CourseKey.from_string(course_key_string)
    course = get_course_with_access(request.user, 'staff', course_key)

    discussion_topics = {}
    discussion_category_map = utils.get_discussion_category_map(
        course,
        request.user,
        divided_only_if_explicit=True,
        exclude_unstarted=False)

    # We extract the data for the course wide discussions from the category map.
    course_wide_entries = discussion_category_map.pop('entries')

    course_wide_children = []
    inline_children = []

    for name, c_type in discussion_category_map['children']:
        if name in course_wide_entries and c_type == TYPE_ENTRY:
            course_wide_children.append([name, c_type])
        else:
            inline_children.append([name, c_type])

    discussion_topics['course_wide_discussions'] = {
        'entries': course_wide_entries,
        'children': course_wide_children
    }

    discussion_category_map['children'] = inline_children
    discussion_topics['inline_discussions'] = discussion_category_map

    return JsonResponse(discussion_topics)
Beispiel #7
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.
            if block == 'handouts' and old_location is None:
                # update handouts location in loc_mapper
                course_location = loc_mapper().translate_locator_to_location(locator, get_course=True)
                old_location = course_location.replace(category='course_info', name=block)
                locator = loc_mapper().translate_location(course_location.course_id, old_location)

            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"
        )
Beispiel #8
0
def videos_post(course, request):
    """
    Input (JSON):
    {
        "files": [{
            "file_name": "video.mp4",
            "content_type": "video/mp4"
        }]
    }

    Returns (JSON):
    {
        "files": [{
            "file_name": "video.mp4",
            "upload_url": "http://example.com/put_video"
        }]
    }

    The returned array corresponds exactly to the input array.
    """
    error = None
    if "files" not in request.json:
        error = "Request object is not JSON or does not contain 'files'"
    elif any("file_name" not in file or "content_type" not in file
             for file in request.json["files"]):
        error = "Request 'files' entry does not contain 'file_name' and 'content_type'"

    if error:
        return JsonResponse({"error": error}, status=400)

    bucket = storage_service_bucket()
    course_video_upload_token = course.video_upload_pipeline[
        "course_video_upload_token"]
    req_files = request.json["files"]
    resp_files = []

    for req_file in req_files:
        file_name = req_file["file_name"]

        edx_video_id = unicode(uuid4())
        key = storage_service_key(bucket, file_name=edx_video_id)
        for metadata_name, value in [
            ("course_video_upload_token", course_video_upload_token),
            ("client_video_id", file_name),
            ("course_key", unicode(course.id)),
        ]:
            key.set_metadata(metadata_name, value)
        upload_url = key.generate_url(
            KEY_EXPIRATION_IN_SECONDS,
            "PUT",
            headers={"Content-Type": req_file["content_type"]})

        # persist edx_video_id in VAL
        create_video({
            "edx_video_id": edx_video_id,
            "status": "upload",
            "client_video_id": file_name,
            "duration": 0,
            "encoded_videos": [],
            "courses": [course.id]
        })

        resp_files.append({
            "file_name": file_name,
            "upload_url": upload_url,
            "edx_video_id": edx_video_id
        })

    return JsonResponse({"files": resp_files}, status=200)
Beispiel #9
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)
        is_read_only = _xblock_is_read_only(component)

        # 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)}))

            # change not authored by requestor but by xblocks.
            store.update_item(component, None)

        elif view_name == 'student_view' and component.has_children:
            context = {
                'runtime_type': 'studio',
                'container_view': False,
                'read_only': is_read_only,
                'root_xblock': component,
            }
            # For non-leaf xblocks on the unit page, show the special rendering
            # which links to the new container page.
            html = render_to_string('container_xblock_component.html', {
                'xblock_context': context,
                '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.
            context = {
                'runtime_type': 'studio',
                'container_view': is_container_view,
                'read_only': is_read_only,
                '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', {
                    'xblock_context': context,
                    'preview': fragment.content,
                    'label': component.display_name or component.scope_ids.block_type,
                })
        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)
Beispiel #10
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 recursively clones the existing item location to a draft location (the draft is
            # implicit, because modulestore is a Draft modulestore)
            _xmodule_recurse(existing_item, lambda i: modulestore().convert_to_draft(i.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)
def _invoke_xblock_handler(request,
                           course_id,
                           usage_id,
                           handler,
                           suffix,
                           course=None):
    """
    Invoke an XBlock handler, either authenticated or not.

    Arguments:
        request (HttpRequest): the current request
        course_id (str): A string of the form org/course/run
        usage_id (str): A string of the form i4x://org/course/category/name@revision
        handler (str): The name of the handler to invoke
        suffix (str): The suffix to pass to the handler when invoked
    """

    # Check submitted files
    files = request.FILES or {}
    error_msg = _check_files_limits(files)
    if error_msg:
        return JsonResponse({'success': error_msg}, status=413)

    # Make a CourseKey from the course_id, raising a 404 upon parse error.
    try:
        course_key = CourseKey.from_string(course_id)
    except InvalidKeyError:
        raise Http404

    set_custom_metrics_for_course_key(course_key)

    with modulestore().bulk_operations(course_key):
        instance, tracking_context = get_module_by_usage_id(request,
                                                            course_id,
                                                            usage_id,
                                                            course=course)

        # Name the transaction so that we can view XBlock handlers separately in
        # New Relic. The suffix is necessary for XModule handlers because the
        # "handler" in those cases is always just "xmodule_handler".
        nr_tx_name = "{}.{}".format(instance.__class__.__name__, handler)
        nr_tx_name += "/{}".format(suffix) if (
            suffix and handler == "xmodule_handler") else ""
        set_monitoring_transaction_name(nr_tx_name,
                                        group="Python/XBlock/Handler")

        tracking_context_name = 'module_callback_handler'
        req = django_to_webob_request(request)
        try:
            with tracker.get_tracker().context(tracking_context_name,
                                               tracking_context):
                resp = instance.handle(handler, req, suffix)
                if suffix == 'problem_check' \
                        and course \
                        and getattr(course, 'entrance_exam_enabled', False) \
                        and getattr(instance, 'in_entrance_exam', False):
                    ee_data = {
                        'entrance_exam_passed':
                        user_has_passed_entrance_exam(request.user, course)
                    }
                    resp = append_data_to_webob_response(resp, ee_data)

        except NoSuchHandlerError:
            log.exception("XBlock %s attempted to access missing handler %r",
                          instance, handler)
            raise Http404

        # If we can't find the module, respond with a 404
        except NotFoundError:
            log.exception(
                "Module indicating to user that request doesn't exist")
            raise Http404

        # For XModule-specific errors, we log the error and respond with an error message
        except ProcessingError as err:
            log.warning(
                "Module encountered an error while processing AJAX call",
                exc_info=True)
            return JsonResponse({'success': err.args[0]}, status=200)

        # If any other error occurred, re-raise it to trigger a 500 response
        except Exception:
            log.exception("error executing xblock handler")
            raise

    return webob_to_django_response(resp)
Beispiel #12
0
def _import_handler(request, courselike_key, root_name, successful_url, context_name, courselike_module, import_func):
    """
    Parameterized function containing the meat of import_handler.
    """
    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':
            raise NotImplementedError('coming soon')
        else:
            # Do everything in a try-except block to make sure everything is properly cleaned up.
            try:
                data_root = path(settings.GITHUB_REPO_ROOT)
                subdir = base64.urlsafe_b64encode(repr(courselike_key))
                course_dir = data_root / subdir
                filename = request.FILES['course-data'].name

                # Use sessions to keep info about import progress
                session_status = request.session.setdefault("import_status", {})
                courselike_string = unicode(courselike_key) + filename
                _save_request_status(request, courselike_string, 0)
                if not filename.endswith('.tar.gz'):
                    _save_request_status(request, courselike_string, -1)
                    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']):
                        _save_request_status(request, courselike_string, -1)
                        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": reverse_course_url('import_handler', courselike_key),
                            "thumbnailUrl": ""
                        }]
                    })
            # Send errors to client with stage at which error occurred.
            except Exception as exception:  # pylint: disable=broad-except
                _save_request_status(request, courselike_string, -1)
                if course_dir.isdir():
                    shutil.rmtree(course_dir)
                    log.info("Course import %s: Temp data cleared", courselike_key)

                log.exception(
                    "error importing course"
                )
                return JsonResponse(
                    {
                        'ErrMsg': str(exception),
                        'Stage': -1
                    },
                    status=400
                )

            # try-finally block for proper clean up after receiving last chunk.
            try:
                # This was the last chunk.
                log.info("Course import %s: Upload complete", courselike_key)
                _save_request_status(request, courselike_string, 1)

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

                log.info("Course import %s: Uploaded file extracted", courselike_key)
                _save_request_status(request, courselike_string, 2)

                # 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

                dirpath = get_dir_for_fname(course_dir, root_name)
                if not dirpath:
                    _save_request_status(request, courselike_string, -2)
                    return JsonResponse(
                        {
                            'ErrMsg': _('Could not find the {0} file in the package.').format(root_name),
                            'Stage': -2
                        },
                        status=415
                    )

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

                log.info("Course import %s: Extracted file verified", courselike_key)
                _save_request_status(request, courselike_string, 3)

                courselike_items = import_func(
                    modulestore(), request.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
                logging.debug('new course at %s', new_location)

                log.info("Course import %s: Course import successful", courselike_key)
                _save_request_status(request, courselike_string, 4)

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

            finally:
                if course_dir.isdir():
                    shutil.rmtree(course_dir)
                    log.info("Course import %s: Temp data cleared", courselike_key)
                # set failed stage number with negative sign in case of unsuccessful import
                if session_status[courselike_string] != 4:
                    _save_request_status(request, courselike_string, -abs(session_status[courselike_string]))

            return JsonResponse({'Status': 'OK'})
    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()
Beispiel #13
0
def login_user(request):
    """
    AJAX request to log in the user.
    """
    third_party_auth_requested = third_party_auth.is_enabled(
    ) and pipeline.running(request)
    first_party_auth_requested = bool(request.POST.get('email')) or bool(
        request.POST.get('password'))
    is_user_third_party_authenticated = False

    try:
        if third_party_auth_requested and not first_party_auth_requested:
            # The user has already authenticated via third-party auth and has not
            # asked to do first party auth by supplying a username or password. We
            # now want to put them through the same logging and cookie calculation
            # logic as with first-party auth.

            # This nested try is due to us only returning an HttpResponse in this
            # one case vs. JsonResponse everywhere else.
            try:
                user = _do_third_party_auth(request)
                is_user_third_party_authenticated = True
            except AuthFailedError as e:
                return HttpResponse(e.value,
                                    content_type="text/plain",
                                    status=403)
        else:
            user = _get_user_by_email(request)

        _check_excessive_login_attempts(user)

        possibly_authenticated_user = user

        if not is_user_third_party_authenticated:
            possibly_authenticated_user = _authenticate_first_party(
                request, user)
            if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login(
            ):
                # Important: This call must be made AFTER the user was successfully authenticated.
                _enforce_password_policy_compliance(
                    request, possibly_authenticated_user)

        if possibly_authenticated_user is None or not possibly_authenticated_user.is_active:
            _handle_failed_authentication(user, possibly_authenticated_user)

        _handle_successful_authentication_and_login(
            possibly_authenticated_user, request)

        redirect_url = None  # The AJAX method calling should know the default destination upon success
        if is_user_third_party_authenticated:
            running_pipeline = pipeline.get(request)
            redirect_url = pipeline.get_complete_url(
                backend_name=running_pipeline['backend'])

        response = JsonResponse({
            'success': True,
            'redirect_url': redirect_url,
        })

        # Ensure that the external marketing site can
        # detect that the user is logged in.
        return set_logged_in_cookies(request, response,
                                     possibly_authenticated_user)
    except AuthFailedError as error:
        log.exception(error.get_response())
        return JsonResponse(error.get_response())
Beispiel #14
0
def post(request, error=""):  # pylint: disable-msg=too-many-statements,unused-argument
    """AJAX request to log in the user."""

    backend_name = None
    email = None
    password = None
    redirect_url = None
    response = None
    running_pipeline = None
    third_party_auth_requested = settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request)
    third_party_auth_successful = False
    trumped_by_first_party_auth = bool(request.POST.get('email')) or bool(request.POST.get('password'))
    user = None

    

    

    if 'email' not in request.POST or 'password' not in request.POST:
        return JsonResponse({
            "success": False,
            "value": _('There was an error receiving your login information. Please email us.'),  # TODO: User error message
        })  # TODO: this should be status code 400  # pylint: disable=fixme

    email = request.POST['email']
    password = request.POST['password']
    try:
        user = User.objects.get(email=email)
    except User.DoesNotExist:
        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
            AUDIT_LOG.warning(u"Login failed - Unknown user email")
        else:
            AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email))

    # see if account has been locked out due to excessive login failures
    user_found_by_email_lookup = user
    if user_found_by_email_lookup and LoginFailures.is_feature_enabled():
        if LoginFailures.is_user_locked_out(user_found_by_email_lookup):
            return JsonResponse({
                "success": False,
                "value": _('This account has been temporarily locked due to excessive login failures. Try again later.'),
            })  # TODO: this should be status code 429  # pylint: disable=fixme

    # see if the user must reset his/her password due to any policy settings
    if PasswordHistory.should_user_reset_password_now(user_found_by_email_lookup):
        return JsonResponse({
            "success": False,
            "value": _('Your password has expired due to password policy on this account. You must '
                       'reset your password before you can log in again. Please click the '
                       '"Forgot Password" link on this page to reset your password before logging in again.'),
        })  # TODO: this should be status code 403  # pylint: disable=fixme

    # if the user doesn't exist, we want to set the username to an invalid
    # username so that authentication is guaranteed to fail and we can take
    # advantage of the ratelimited backend
    username = user.username if user else ""

    if not third_party_auth_successful:
        try:
            user = authenticate(username=username, password=password, request=request)
        # this occurs when there are too many attempts from the same IP address
        except RateLimitException:
            return JsonResponse({
                "success": False,
                "value": _('Too many failed login attempts. Try again later.'),
            })  # TODO: this should be status code 429  # pylint: disable=fixme

    if user is None:
        # tick the failed login counters if the user exists in the database
        if user_found_by_email_lookup and LoginFailures.is_feature_enabled():
            LoginFailures.increment_lockout_counter(user_found_by_email_lookup)

        # if we didn't find this username earlier, the account for this email
        # doesn't exist, and doesn't have a corresponding password
        if username != "":
            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
                loggable_id = user_found_by_email_lookup.id if user_found_by_email_lookup else "<unknown>"
                AUDIT_LOG.warning(u"Login failed - password for user.id: {0} is invalid".format(loggable_id))
            else:
                AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(email))
        return JsonResponse({
            "success": False,
            "value": _('Email or password is incorrect.'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme

    # successful login, clear failed login attempts counters, if applicable
    if LoginFailures.is_feature_enabled():
        LoginFailures.clear_lockout_counter(user)

    if user is not None and user.is_active:
        try:
            # We do not log here, because we have a handler registered
            # to perform logging on successful logins.
            login(request, user)
            if request.POST.get('remember') == 'true':
                request.session.set_expiry(604800)
                log.debug("Setting user session to never expire")
            else:
                request.session.set_expiry(0)
        except Exception as e:
            AUDIT_LOG.critical("Login failed - Could not create session. Is memcached running?")
            log.critical("Login failed - Could not create session. Is memcached running?")
            log.exception(e)
            raise

        
        response = JsonResponse({
            "success": True,
            "username": user.username,
            "email": user.email
        })

        # set the login cookie for the edx marketing site 
        # we want this cookie to be accessed via javascript
        # so httponly is set to None

        if request.session.get_expire_at_browser_close():
            max_age = None
            expires = None
        else:
            max_age = request.session.get_expiry_age()
            expires_time = time.time() + max_age
            expires = cookie_date(expires_time)

        response.set_cookie(
            settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age,
            expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
            path='/', secure=None, httponly=None,
        )

        return response

    if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
        AUDIT_LOG.warning(u"Login failed - Account not active for user.id: {0}, resending activation".format(user.id))
    else:
        AUDIT_LOG.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))

    reactivation_email_for_user(user)
    not_activated_msg = _("This account has not been activated. We have sent another activation message. Please check your e-mail for the activation instructions.")
    return JsonResponse({
        "success": False,
        "value": not_activated_msg,
    })  # TODO: this should be status code 400  # pylint: disable=fixme
Beispiel #15
0
def create_order(request):
    """
    Submit PhotoVerification and create a new Order for this verified cert
    """
    if not SoftwareSecurePhotoVerification.user_has_valid_or_pending(
            request.user):
        attempt = SoftwareSecurePhotoVerification(user=request.user)
        try:
            b64_face_image = request.POST['face_image'].split(",")[1]
            b64_photo_id_image = request.POST['photo_id_image'].split(",")[1]
        except IndexError:
            context = {
                'success': False,
            }
            return JsonResponse(context)
        attempt.upload_face_image(b64_face_image.decode('base64'))
        attempt.upload_photo_id_image(b64_photo_id_image.decode('base64'))
        attempt.mark_ready()

        attempt.save()

    course_id = request.POST['course_id']
    course_id = CourseKey.from_string(course_id)
    donation_for_course = request.session.get('donation_for_course', {})
    current_donation = donation_for_course.get(unicode(course_id),
                                               decimal.Decimal(0))
    contribution = request.POST.get(
        "contribution", donation_for_course.get(unicode(course_id), 0))
    try:
        amount = decimal.Decimal(contribution).quantize(
            decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
    except decimal.InvalidOperation:
        return HttpResponseBadRequest(_("Selected price is not valid number."))

    if amount != current_donation:
        donation_for_course[unicode(course_id)] = amount
        request.session['donation_for_course'] = donation_for_course

    # prefer professional mode over verified_mode
    current_mode = CourseMode.verified_mode_for_course(course_id)

    # make sure this course has a verified mode
    if not current_mode:
        return HttpResponseBadRequest(
            _("This course doesn't support verified certificates"))

    if current_mode.slug == 'professional':
        amount = current_mode.min_price

    if amount < current_mode.min_price:
        return HttpResponseBadRequest(
            _("No selected price or selected price is below minimum."))

    # I know, we should check this is valid. All kinds of stuff missing here
    cart = Order.get_cart_for_user(request.user)
    cart.clear()
    enrollment_mode = current_mode.slug
    CertificateItem.add_to_order(cart, course_id, amount, enrollment_mode)

    # Change the order's status so that we don't accidentally modify it later.
    # We need to do this to ensure that the parameters we send to the payment system
    # match what we store in the database.
    # (Ordinarily we would do this client-side when the user submits the form, but since
    # the JavaScript on this page does that immediately, we make the change here instead.
    # This avoids a second AJAX call and some additional complication of the JavaScript.)
    # If a user later re-enters the verification / payment flow, she will create a new order.
    cart.start_purchase()

    callback_url = request.build_absolute_uri(
        reverse("shoppingcart.views.postpay_callback"))

    params = get_signed_purchase_params(cart, callback_url=callback_url)

    params['success'] = True
    params['merchant_defined_data1'] = unicode(course_id)
    return HttpResponse(json.dumps(params), content_type="text/json")
Beispiel #16
0
def _assets_json(request, course_key):
    """
    Display an editable asset library.

    Supports start (0-based index into the list of assets) and max query parameters.
    """
    requested_page = int(request.REQUEST.get('page', 0))
    requested_page_size = int(request.REQUEST.get('page_size', 50))
    requested_sort = request.REQUEST.get('sort', 'date_added')
    sort_direction = DESCENDING
    if request.REQUEST.get('direction', '').lower() == 'asc':
        sort_direction = ASCENDING

    # Convert the field name to the Mongo name
    if requested_sort == 'date_added':
        requested_sort = 'uploadDate'
    elif requested_sort == 'display_name':
        requested_sort = 'displayname'
    sort = [(requested_sort, sort_direction)]

    current_page = max(requested_page, 0)
    start = current_page * requested_page_size
    assets, total_count = _get_assets_for_page(request, course_key,
                                               current_page,
                                               requested_page_size, sort)
    end = start + len(assets)

    # If the query is beyond the final page, then re-query the final page so that at least one asset is returned
    if requested_page > 0 and start >= total_count:
        current_page = int(math.floor((total_count - 1) / requested_page_size))
        start = current_page * requested_page_size
        assets, total_count = _get_assets_for_page(request, course_key,
                                                   current_page,
                                                   requested_page_size, sort)
        end = start + len(assets)

    asset_json = []
    for asset in assets:
        asset_id = asset['_id']
        asset_location = StaticContent.compute_location(
            course_key, asset_id['name'])
        # note, due to the schema change we may not have a 'thumbnail_location' in the result set
        thumbnail_location = asset.get('thumbnail_location', None)
        if thumbnail_location:
            thumbnail_location = course_key.make_asset_key(
                'thumbnail', thumbnail_location[4])

        asset_locked = asset.get('locked', False)
        asset_json.append(
            _get_asset_json(asset['displayname'], asset['uploadDate'],
                            asset_location, thumbnail_location, asset_locked))

    return JsonResponse({
        'start': start,
        'end': end,
        'page': current_page,
        'pageSize': requested_page_size,
        'totalCount': total_count,
        'assets': asset_json,
        'sort': requested_sort,
    })
def search_certificates(request):
    """
    Search for certificates for a particular user OR along with the given course.

    Supports search by either username or email address along with course id.

    First filter the records for the given username/email and then filter against the given course id (if given).
    Show the 'Regenerate' button if a record found in 'generatedcertificate' model otherwise it will show the Generate
    button.

    Arguments:
        request (HttpRequest): The request object.

    Returns:
        JsonResponse

    Example Usage:
        GET /certificates/[email protected]
        GET /certificates/[email protected]&course_id=xyz

        Response: 200 OK
        Content-Type: application/json
        [
            {
                "username": "******",
                "course_key": "edX/DemoX/Demo_Course",
                "type": "verified",
                "status": "downloadable",
                "download_url": "http://www.example.com/cert.pdf",
                "grade": "0.98",
                "created": 2015-07-31T00:00:00Z,
                "modified": 2015-07-31T00:00:00Z
            }
        ]

    """
    unbleached_filter = six.moves.urllib.parse.unquote(
        six.moves.urllib.parse.quote_plus(request.GET.get("user", "")))
    user_filter = bleach.clean(unbleached_filter)
    if not user_filter:
        msg = _("user is not given.")
        return HttpResponseBadRequest(msg)

    try:
        user = User.objects.get(Q(email=user_filter) | Q(username=user_filter))
    except User.DoesNotExist:
        return HttpResponseBadRequest(
            _(u"user '{user}' does not exist").format(user=user_filter))

    certificates = api.get_certificates_for_user(user.username)
    for cert in certificates:
        cert["course_key"] = six.text_type(cert["course_key"])
        cert["created"] = cert["created"].isoformat()
        cert["modified"] = cert["modified"].isoformat()
        cert["regenerate"] = not cert['is_pdf_certificate']

    course_id = six.moves.urllib.parse.quote_plus(request.GET.get(
        "course_id", ""),
                                                  safe=':/')
    if course_id:
        try:
            course_key = CourseKey.from_string(course_id)
        except InvalidKeyError:
            return HttpResponseBadRequest(
                _(u"Course id '{course_id}' is not valid").format(
                    course_id=course_id))
        else:
            try:
                if CourseOverview.get_from_id(course_key):
                    certificates = [
                        certificate for certificate in certificates
                        if certificate['course_key'] == course_id
                    ]
                    if not certificates:
                        return JsonResponse([{
                            'username': user.username,
                            'course_key': course_id,
                            'regenerate': False
                        }])
            except CourseOverview.DoesNotExist:
                msg = _(
                    u"The course does not exist against the given key '{course_key}'"
                ).format(course_key=course_key)
                return HttpResponseBadRequest(msg)

    return JsonResponse(certificates)
Beispiel #18
0
 def test_get_sailthru_list_map_no_list(self, mock_sailthru_client):
     """Test when no list returned from sailthru"""
     mock_sailthru_client.api_get.return_value = SailthruResponse(JsonResponse({'lists': []}))
     self.assertEqual(_get_list_from_email_marketing_provider(mock_sailthru_client), {})
     mock_sailthru_client.api_get.assert_called_with("list", {})
Beispiel #19
0
def _upload_asset(request, location):
    '''
    This method allows for POST uploading of files into the course asset
    library, which will be supported by GridFS in MongoDB.
    '''
    old_location = loc_mapper().translate_locator_to_location(location)

    # Does the course actually exist?!? Get anything from it to prove its
    # existence
    try:
        modulestore().get_item(old_location)
    except:
        # no return it as a Bad Request response
        logging.error('Could not find course' + old_location)
        return HttpResponseBadRequest()

    # compute a 'filename' which is similar to the location formatting, we're
    # using the 'filename' nomenclature since we're using a FileSystem paradigm
    # here. We're just imposing the Location string formatting expectations to
    # keep things a bit more consistent
    upload_file = request.FILES['file']
    filename = upload_file.name
    mime_type = upload_file.content_type

    content_loc = StaticContent.compute_location(old_location.org, old_location.course, filename)

    chunked = upload_file.multiple_chunks()
    sc_partial = partial(StaticContent, content_loc, filename, mime_type)
    if chunked:
        content = sc_partial(upload_file.chunks())
        tempfile_path = upload_file.temporary_file_path()
    else:
        content = sc_partial(upload_file.read())
        tempfile_path = None

    # first let's see if a thumbnail can be created
    (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
            content,
            tempfile_path=tempfile_path
    )

    # delete cached thumbnail even if one couldn't be created this time (else
    # the old thumbnail will continue to show)
    del_cached_content(thumbnail_location)
    # now store thumbnail location only if we could create it
    if thumbnail_content is not None:
        content.thumbnail_location = thumbnail_location

    # then commit the content
    contentstore().save(content)
    del_cached_content(content.location)

    # readback the saved content - we need the database timestamp
    readback = contentstore().find(content.location)

    locked = getattr(content, 'locked', False)
    response_payload = {
        'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location, locked),
        'msg': _('Upload completed')
    }

    return JsonResponse(response_payload)
Beispiel #20
0
 def test_create_sailthru_list_error(self, mock_sailthru_client):
     """Test error occurrence while creating sailthru list"""
     mock_sailthru_client.api_post.return_value = SailthruResponse(
         JsonResponse({'error': 43, 'errormsg': 'Got an error'})
     )
     self.assertEqual(_create_user_list(mock_sailthru_client, 'test_list_name'), False)
Beispiel #21
0
def videos_post(course, request):
    """
    Input (JSON):
    {
        "files": [{
            "file_name": "video.mp4",
            "content_type": "video/mp4"
        }]
    }

    Returns (JSON):
    {
        "files": [{
            "file_name": "video.mp4",
            "upload_url": "http://example.com/put_video"
        }]
    }

    The returned array corresponds exactly to the input array.
    """
    error = None
    data = request.json
    if 'files' not in data:
        error = "Request object is not JSON or does not contain 'files'"
    elif any(
            'file_name' not in file or 'content_type' not in file
            for file in data['files']
    ):
        error = "Request 'files' entry does not contain 'file_name' and 'content_type'"
    elif any(
            file['content_type'] not in VIDEO_SUPPORTED_FILE_FORMATS.values()
            for file in data['files']
    ):
        error = "Request 'files' entry contain unsupported content_type"

    if error:
        return JsonResponse({'error': error}, status=400)

    bucket = storage_service_bucket()
    req_files = data['files']
    resp_files = []

    for req_file in req_files:
        file_name = req_file['file_name']

        try:
            file_name.encode('ascii')
        except UnicodeEncodeError:
            error_msg = 'The file name for %s must contain only ASCII characters.' % file_name
            return JsonResponse({'error': error_msg}, status=400)

        edx_video_id = unicode(uuid4())
        key = storage_service_key(bucket, file_name=edx_video_id)

        metadata_list = [
            ('client_video_id', file_name),
            ('course_key', unicode(course.id)),
        ]

        # Only include `course_video_upload_token` if its set, as it won't be required if video uploads
        # are enabled by default.
        course_video_upload_token = course.video_upload_pipeline.get('course_video_upload_token')
        if course_video_upload_token:
            metadata_list.append(('course_video_upload_token', course_video_upload_token))

        is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id)
        if is_video_transcript_enabled:
            transcript_preferences = get_transcript_preferences(unicode(course.id))
            if transcript_preferences is not None:
                metadata_list.append(('transcript_preferences', json.dumps(transcript_preferences)))

        for metadata_name, value in metadata_list:
            key.set_metadata(metadata_name, value)
        upload_url = key.generate_url(
            KEY_EXPIRATION_IN_SECONDS,
            'PUT',
            headers={'Content-Type': req_file['content_type']}
        )

        # persist edx_video_id in VAL
        create_video({
            'edx_video_id': edx_video_id,
            'status': 'upload',
            'client_video_id': file_name,
            'duration': 0,
            'encoded_videos': [],
            'courses': [unicode(course.id)]
        })

        resp_files.append({'file_name': file_name, 'upload_url': upload_url, 'edx_video_id': edx_video_id})

    return JsonResponse({'files': resp_files}, status=200)
Beispiel #22
0
def course_discussions_settings_handler(request, course_key_string):
    """
    The restful handler for divided discussion setting requests. Requires JSON.
    This will raise 404 if user is not staff.
    GET
        Returns the JSON representation of divided discussion settings for the course.
    PATCH
        Updates the divided discussion settings for the course. Returns the JSON representation of updated settings.
    """
    course_key = CourseKey.from_string(course_key_string)
    course = get_course_with_access(request.user, 'staff', course_key)
    discussion_settings = get_course_discussion_settings(course_key)

    if request.method == 'PATCH':
        divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
            course, discussion_settings)

        settings_to_change = {}

        if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json:
            divided_course_wide_discussions = request.json.get(
                'divided_course_wide_discussions',
                divided_course_wide_discussions)
            divided_inline_discussions = request.json.get(
                'divided_inline_discussions', divided_inline_discussions)
            settings_to_change[
                'divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions

        if 'always_divide_inline_discussions' in request.json:
            settings_to_change[
                'always_divide_inline_discussions'] = request.json.get(
                    'always_divide_inline_discussions')
        if 'division_scheme' in request.json:
            settings_to_change['division_scheme'] = request.json.get(
                'division_scheme')

        if not settings_to_change:
            return JsonResponse({"error": unicode("Bad Request")}, 400)

        try:
            if settings_to_change:
                discussion_settings = set_course_discussion_settings(
                    course_key, **settings_to_change)

        except ValueError as err:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse({"error": unicode(err)}, 400)

    divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
        course, discussion_settings)

    return JsonResponse({
        'id':
        discussion_settings.id,
        'divided_inline_discussions':
        divided_inline_discussions,
        'divided_course_wide_discussions':
        divided_course_wide_discussions,
        'always_divide_inline_discussions':
        discussion_settings.always_divide_inline_discussions,
        'division_scheme':
        discussion_settings.division_scheme,
        'available_division_schemes':
        available_division_schemes(course_key)
    })
Beispiel #23
0
def certificates_list_handler(request, course_key_string):
    """
    A RESTful handler for Course Certificates

    GET
        html: return Certificates list page (Backbone application)
    POST
        json: create new Certificate
    """
    course_key = CourseKey.from_string(course_key_string)
    store = modulestore()
    with store.bulk_operations(course_key):
        try:
            course = _get_course_and_check_access(course_key, request.user)
        except PermissionDenied:
            msg = _('PermissionDenied: Failed in authenticating {user}'
                    ).format(user=request.user)
            return JsonResponse({"error": msg}, status=403)

        if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
            certificate_url = reverse_course_url(
                'certificates.certificates_list_handler', course_key)
            course_outline_url = reverse_course_url('course_handler',
                                                    course_key)
            upload_asset_url = reverse_course_url('assets_handler', course_key)
            activation_handler_url = reverse_course_url(
                handler_name='certificates.certificate_activation_handler',
                course_key=course_key)
            course_modes = [
                mode.slug
                for mode in CourseMode.modes_for_course(course_id=course.id,
                                                        include_expired=True)
                if mode.slug != 'audit'
            ]
            if len(course_modes) > 0:
                certificate_web_view_url = get_lms_link_for_certificate_web_view(
                    user_id=request.user.id,
                    course_key=course_key,
                    mode=course_modes[
                        0]  # CourseMode.modes_for_course returns default mode if doesn't find anyone.
                )
            else:
                certificate_web_view_url = None
            certificates = None
            is_active = False
            if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
                certificates = CertificateManager.get_certificates(course)
                # we are assuming only one certificate in certificates collection.
                for certificate in certificates:
                    is_active = certificate.get('is_active', False)
                    break

            return render_to_response(
                'certificates.html', {
                    'context_course': course,
                    'certificate_url': certificate_url,
                    'course_outline_url': course_outline_url,
                    'upload_asset_url': upload_asset_url,
                    'certificates': certificates,
                    'course_modes': course_modes,
                    'certificate_web_view_url': certificate_web_view_url,
                    'is_active': is_active,
                    'is_global_staff': GlobalStaff().has_user(request.user),
                    'certificate_activation_handler_url':
                    activation_handler_url
                })
        elif "application/json" in request.META.get('HTTP_ACCEPT'):
            # Retrieve the list of certificates for the specified course
            if request.method == 'GET':
                certificates = CertificateManager.get_certificates(course)
                return JsonResponse(certificates, encoder=EdxJSONEncoder)
            elif request.method == 'POST':
                # Add a new certificate to the specified course
                try:
                    new_certificate = CertificateManager.deserialize_certificate(
                        course, request.body)
                except CertificateValidationError as err:
                    return JsonResponse({"error": err.message}, status=400)
                if course.certificates.get('certificates') is None:
                    course.certificates['certificates'] = []
                course.certificates['certificates'].append(
                    new_certificate.certificate_data)
                response = JsonResponse(
                    CertificateManager.serialize_certificate(new_certificate),
                    status=201)
                response["Location"] = reverse_course_url(
                    'certificates.certificates_detail_handler',
                    course.id,
                    kwargs={'certificate_id': new_certificate.id})
                store.update_item(course, request.user.id)
                CertificateManager.track_event(
                    'created', {
                        'course_id': unicode(course.id),
                        'configuration_id': new_certificate.id
                    })
                course = _get_course_and_check_access(course_key, request.user)
                return response
        else:
            return HttpResponse(status=406)
Beispiel #24
0
def _update_asset(request, location, asset_id):
    """
    restful CRUD operations for a course asset.
    Currently only DELETE, POST, and PUT methods are implemented.

    asset_id: the URL of the asset (used by Backbone as the id)
    """
    def get_asset_location(asset_id):
        """ Helper method to get the location (and verify it is valid). """
        try:
            return StaticContent.get_location_from_path(asset_id)
        except InvalidLocationError as err:
            # return a 'Bad Request' to browser as we have a malformed Location
            return JsonResponse({"error": err.message}, status=400)

    if request.method == 'DELETE':
        loc = get_asset_location(asset_id)
        # Make sure the item to delete actually exists.
        try:
            content = contentstore().find(loc)
        except NotFoundError:
            return JsonResponse(status=404)

        # ok, save the content into the trashcan
        contentstore('trashcan').save(content)

        # see if there is a thumbnail as well, if so move that as well
        if content.thumbnail_location is not None:
            try:
                thumbnail_content = contentstore().find(
                    content.thumbnail_location)
                contentstore('trashcan').save(thumbnail_content)
                # hard delete thumbnail from origin
                contentstore().delete(thumbnail_content.get_id())
                # remove from any caching
                del_cached_content(thumbnail_content.location)
            except:
                logging.warning('Could not delete thumbnail: %s',
                                content.thumbnail_location)

        # delete the original
        contentstore().delete(content.get_id())
        # remove from cache
        del_cached_content(content.location)
        return JsonResponse()

    elif request.method in ('PUT', 'POST'):
        if 'file' in request.FILES:
            return _upload_asset(request, location)
        else:
            # Update existing asset
            try:
                modified_asset = json.loads(request.body)
            except ValueError:
                return HttpResponseBadRequest()
            asset_id = modified_asset['url']
            asset_location = get_asset_location(asset_id)
            contentstore().set_attr(asset_location, 'locked',
                                    modified_asset['locked'])
            # Delete the asset from the cache so we check the lock status the next time it is requested.
            del_cached_content(asset_location)
            return JsonResponse(modified_asset, status=201)
Beispiel #25
0
def certificates_detail_handler(request, course_key_string, certificate_id):
    """
    JSON API endpoint for manipulating a course certificate via its internal identifier.
    Utilized by the Backbone.js 'certificates' application model

    POST or PUT
        json: update the specified certificate based on provided information
    DELETE
        json: remove the specified certificate from the course
    """
    course_key = CourseKey.from_string(course_key_string)
    course = _get_course_and_check_access(course_key, request.user)

    certificates_list = course.certificates.get('certificates', [])
    match_index = None
    match_cert = None
    for index, cert in enumerate(certificates_list):
        if certificate_id is not None:
            if int(cert['id']) == int(certificate_id):
                match_index = index
                match_cert = cert

    store = modulestore()
    if request.method in ('POST', 'PUT'):
        if certificate_id:
            active_certificates = CertificateManager.get_certificates(
                course, only_active=True)
            if int(certificate_id) in [
                    int(certificate["id"])
                    for certificate in active_certificates
            ]:
                # Only global staff (PMs) are able to edit active certificate configuration
                if not GlobalStaff().has_user(request.user):
                    raise PermissionDenied()
        try:
            new_certificate = CertificateManager.deserialize_certificate(
                course, request.body)
        except CertificateValidationError as err:
            return JsonResponse({"error": err.message}, status=400)

        serialized_certificate = CertificateManager.serialize_certificate(
            new_certificate)
        cert_event_type = 'created'
        if match_cert:
            cert_event_type = 'modified'
            certificates_list[match_index] = serialized_certificate
        else:
            certificates_list.append(serialized_certificate)

        store.update_item(course, request.user.id)
        CertificateManager.track_event(
            cert_event_type, {
                'course_id': unicode(course.id),
                'configuration_id': serialized_certificate["id"]
            })
        return JsonResponse(serialized_certificate, status=201)

    elif request.method == "DELETE":
        if not match_cert:
            return JsonResponse(status=404)

        active_certificates = CertificateManager.get_certificates(
            course, only_active=True)
        if int(certificate_id) in [
                int(certificate["id"]) for certificate in active_certificates
        ]:
            # Only global staff (PMs) are able to delete active certificate configuration
            if not GlobalStaff().has_user(request.user):
                raise PermissionDenied()

        CertificateManager.remove_certificate(request=request,
                                              store=store,
                                              course=course,
                                              certificate_id=certificate_id)
        CertificateManager.track_event('deleted', {
            'course_id': unicode(course.id),
            'configuration_id': certificate_id
        })
        return JsonResponse(status=204)
Beispiel #26
0
def upload_transcripts(request):
    """
    Upload transcripts for current module.

    returns: response dict::

        status: 'Success' and HTTP 200 or 'Error' and HTTP 400.
        subs: Value of uploaded and saved html5 sub field in video item.
    """
    response = {
        'status': 'Unknown server error',
        'subs': '',
    }

    locator = request.POST.get('locator')
    if not locator:
        return error_response(response,
                              'POST data without "locator" form data.')

    try:
        item = _get_item(request, request.POST)
    except (ItemNotFoundError, InvalidLocationError,
            InsufficientSpecificationError):
        return error_response(response, "Can't find item by locator.")

    if 'file' not in request.FILES:
        return error_response(response, 'POST data without "file" form data.')

    video_list = request.POST.get('video_list')
    if not video_list:
        return error_response(response, 'POST data without video names.')

    try:
        video_list = json.loads(video_list)
    except ValueError:
        return error_response(response, 'Invalid video_list JSON.')

    source_subs_filedata = request.FILES['file'].read().decode('utf8')
    source_subs_filename = request.FILES['file'].name

    if '.' not in source_subs_filename:
        return error_response(response, "Undefined file extension.")

    basename = os.path.basename(source_subs_filename)
    source_subs_name = os.path.splitext(basename)[0]
    source_subs_ext = os.path.splitext(basename)[1][1:]

    if item.category != 'video':
        return error_response(
            response, 'Transcripts are supported only for "video" modules.')

    # Allow upload only if any video link is presented
    if video_list:
        sub_attr = source_subs_name

        try:  # Generate and save for 1.0 speed, will create subs_sub_attr.srt.sjson subtitles file in storage.
            generate_subs_from_source({1: sub_attr}, source_subs_ext,
                                      source_subs_filedata, item)
        except TranscriptsGenerationException as e:
            return error_response(response, e.message)
        statuses = {}
        for video_dict in video_list:
            video_name = video_dict['video']
            # We are creating transcripts for every video source,
            # for the case that in future, some of video sources can be deleted.
            statuses[video_name] = copy_or_rename_transcript(
                video_name, sub_attr, item)
            try:
                # updates item.sub with `video_name` if it is successful.
                copy_or_rename_transcript(video_name, sub_attr, item)
                selected_name = video_name  # name to write to item.sub field, chosen at random.
            except NotFoundError:
                # subtitles file `sub_attr` is not presented in the system. Nothing to copy or rename.
                return error_response(
                    response,
                    "Can't find transcripts in storage for {}".format(
                        sub_attr))

        item.sub = selected_name  # write one of  new subtitles names to item.sub attribute.
        save_module(item)
        response['subs'] = item.sub
        response['status'] = 'Success'
    else:
        return error_response(response, 'Empty video sources.')

    return JsonResponse(response)
Beispiel #27
0
def add_coupon(request, course_id):  # pylint: disable=unused-argument
    """
    add coupon in the Coupons Table
    """
    code = request.POST.get('code')

    # check if the code is already in the Coupons Table and active
    try:
        course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
        coupon = Coupon.objects.get(is_active=True,
                                    code=code,
                                    course_id=course_id)
    except Coupon.DoesNotExist:
        # check if the coupon code is in the CourseRegistrationCode Table
        course_registration_code = CourseRegistrationCode.objects.filter(
            code=code)
        if course_registration_code:
            return JsonResponse(
                {
                    'message':
                    _("The code ({code}) that you have tried to define is already in use as a registration code"
                      ).format(code=code)
                },
                status=400)  # status code 400: Bad Request

        description = request.POST.get('description')
        course_id = request.POST.get('course_id')
        try:
            discount = int(request.POST.get('discount'))
        except ValueError:
            return JsonResponse(
                {
                    'message':
                    _("Please Enter the Integer Value for Coupon Discount")
                },
                status=400)  # status code 400: Bad Request

        if discount > 100 or discount < 0:
            return JsonResponse(
                {
                    'message':
                    _("Please Enter the Coupon Discount Value Less than or Equal to 100"
                      )
                },
                status=400)  # status code 400: Bad Request
        expiration_date = None
        if request.POST.get('expiration_date'):
            expiration_date = request.POST.get('expiration_date')
            try:
                expiration_date = datetime.datetime.strptime(
                    expiration_date, "%m/%d/%Y").replace(
                        tzinfo=pytz.UTC) + datetime.timedelta(days=1)
            except ValueError:
                return JsonResponse(
                    {
                        'message':
                        _("Please enter the date in this format i-e month/day/year"
                          )
                    },
                    status=400)  # status code 400: Bad Request

        coupon = Coupon(code=code,
                        description=description,
                        course_id=course_id,
                        percentage_discount=discount,
                        created_by_id=request.user.id,
                        expiration_date=expiration_date)
        coupon.save()
        return JsonResponse({
            'message':
            _("coupon with the coupon code ({code}) added successfully").
            format(code=code)
        })

    if coupon:
        return JsonResponse(
            {
                'message':
                _("coupon with the coupon code ({code}) already exists for this course"
                  ).format(code=code)
            },
            status=400)  # status code 400: Bad Request
Beispiel #28
0
def _save_xblock(user,
                 xblock,
                 data=None,
                 children_strings=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).
    """
    store = modulestore()
    # Perform all xblock changes within a (single-versioned) transaction
    with store.bulk_operations(xblock.location.course_key):

        # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI).
        if publish == "discard_changes":
            store.revert_to_published(xblock.location, user.id)
            # Returning the same sort of result that we do for other save operations. In the future,
            # we may want to return the full XBlockInfo.
            return JsonResponse({'id': unicode(xblock.location)})

        old_metadata = own_metadata(xblock)
        old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)

        if data:
            # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
            xblock.data = data
        else:
            data = old_content['data'] if 'data' in old_content else None

        if children_strings is not None:
            children = []
            for child_string in children_strings:
                children.append(usage_key_with_run(child_string))

            # if new children have been added, remove them from their old parents
            new_children = set(children) - set(xblock.children)
            for new_child in new_children:
                old_parent_location = store.get_parent_location(new_child)
                if old_parent_location:
                    old_parent = store.get_item(old_parent_location)
                    old_parent.children.remove(new_child)
                    old_parent = _update_with_callback(old_parent, user)
                else:
                    # the Studio UI currently doesn't present orphaned children, so assume this is an error
                    return JsonResponse(
                        {
                            "error":
                            "Invalid data, possibly caused by concurrent authors."
                        }, 400)

            # make sure there are no old children that became orphans
            # In a single-author (no-conflict) scenario, all children in the persisted list on the server should be
            # present in the updated list.  If there are any children that have been dropped as part of this update,
            # then that would be an error.
            #
            # We can be even more restrictive in a multi-author (conflict), by returning an error whenever
            # len(old_children) > 0. However, that conflict can still be "merged" if the dropped child had been
            # re-parented. Hence, the check for the parent in the any statement below.
            #
            # Note that this multi-author conflict error should not occur in modulestores (such as Split) that support
            # atomic write transactions.  In Split, if there was another author who moved one of the "old_children"
            # into another parent, then that child would have been deleted from this parent on the server. However,
            # this is error could occur in modulestores (such as Draft) that do not support atomic write-transactions
            old_children = set(xblock.children) - set(children)
            if any(
                    store.get_parent_location(old_child) == xblock.location
                    for old_child in old_children):
                # since children are moved as part of a single transaction, orphans should not be created
                return JsonResponse(
                    {
                        "error":
                        "Invalid data, possibly caused by concurrent authors."
                    }, 400)

            # set the children on the xblock
            xblock.children = children

        # 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(xblock, 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 = xblock.fields[metadata_key]

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

        # update the xblock and call any xblock callbacks
        xblock = _update_with_callback(xblock, user, old_metadata, old_content)

        # for static tabs, their containing course also records their display name
        if xblock.location.category == 'static_tab':
            course = store.get_course(xblock.location.course_key)
            # find the course's reference to this tab and update the name.
            static_tab = CourseTabList.get_tab_by_slug(course.tabs,
                                                       xblock.location.name)
            # only update if changed
            if static_tab and static_tab['name'] != xblock.display_name:
                static_tab['name'] = xblock.display_name
                store.update_item(course, user.id)

        result = {
            'id': unicode(xblock.location),
            'data': data,
            'metadata': own_metadata(xblock)
        }

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

        # If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
        # then this item should be republished. This is used by staff locking to ensure that changing the draft
        # value of the staff lock will also update the published version, but only at the unit level.
        if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
            if modulestore().has_published_version(xblock):
                publish = 'make_public'

        # Make public after updating the xblock, in case the caller asked for both an update and a publish.
        # Used by Bok Choy tests and by republishing of staff locks.
        if publish == 'make_public':
            modulestore().publish(xblock.location, user.id)

        # Note that children aren't being returned until we have a use case.
        return JsonResponse(result, encoder=EdxJSONEncoder)
Beispiel #29
0
def footer(request):
    """Retrieve the branded footer.

    This end-point provides information about the site footer,
    allowing for consistent display of the footer across other sites
    (for example, on the marketing site and blog).

    It can be used in one of two ways:
    1) A client renders the footer from a JSON description.
    2) A browser loads an HTML representation of the footer
        and injects it into the DOM.  The HTML includes
        CSS and JavaScript links.

    In case (2), we assume that the following dependencies
    are included on the page:
    a) JQuery (same version as used in edx-platform)
    b) font-awesome (same version as used in edx-platform)
    c) Open Sans web fonts

    Example: Retrieving the footer as JSON

        GET /api/branding/v1/footer
        Accepts: application/json

        {
            "navigation_links": [
                {
                  "url": "http://example.com/about",
                  "name": "about",
                  "title": "About"
                },
                # ...
            ],
            "social_links": [
                {
                    "url": "http://example.com/social",
                    "name": "facebook",
                    "icon-class": "fa-facebook-square",
                    "title": "Facebook",
                    "action": "Sign up on Facebook!"
                },
                # ...
            ],
            "mobile_links": [
                {
                    "url": "http://example.com/android",
                    "name": "google",
                    "image": "http://example.com/google.png",
                    "title": "Google"
                },
                # ...
            ],
            "legal_links": [
                {
                    "url": "http://example.com/terms-of-service.html",
                    "name": "terms_of_service",
                    "title': "Terms of Service"
                },
                # ...
            ],
            "openedx_link": {
                "url": "http://open.edx.org",
                "title": "Powered by Open edX",
                "image": "http://example.com/openedx.png"
            },
            "logo_image": "http://example.com/static/images/default-theme/logo.png",
            "copyright": "EdX, Open edX, and the edX and Open edX logos are \
                registered trademarks or trademarks of edX Inc."
        }


    Example: Retrieving the footer as HTML

        GET /api/branding/v1/footer
        Accepts: text/html


    Example: Including the footer with the "Powered by OpenEdX" logo

        GET /api/branding/v1/footer?show-openedx-logo=1
        Accepts: text/html


    Example: Retrieving the footer in a particular language

        GET /api/branding/v1/footer?language=en
        Accepts: text/html

    Example: Retrieving the footer with all JS and CSS dependencies (for testing)

        GET /api/branding/v1/footer?include-dependencies=1
        Accepts: text/html

    """
    if not branding_api.is_enabled():
        raise Http404

    # Use the content type to decide what representation to serve
    accepts = request.META.get('HTTP_ACCEPT', '*/*')

    # Show the OpenEdX logo in the footer
    show_openedx_logo = bool(request.GET.get('show-openedx-logo', False))

    # Include JS and CSS dependencies
    # This is useful for testing the end-point directly.
    include_dependencies = bool(request.GET.get('include-dependencies', False))

    # Override the language if necessary
    language = request.GET.get('language', translation.get_language())

    # Render the footer information based on the extension
    if 'text/html' in accepts or '*/*' in accepts:
        cache_key = u"branding.footer.{params}.html".format(
            params=urllib.urlencode(
                {
                    'language': language,
                    'show_openedx_logo': show_openedx_logo,
                    'include_dependencies': include_dependencies,
                }))
        content = cache.get(cache_key)
        if content is None:
            with translation.override(language):
                content = _render_footer_html(request, show_openedx_logo,
                                              include_dependencies)
                cache.set(cache_key, content, settings.FOOTER_CACHE_TIMEOUT)
        return HttpResponse(content,
                            status=200,
                            content_type="text/html; charset=utf-8")

    elif 'application/json' in accepts:
        cache_key = u"branding.footer.{params}.json".format(
            params=urllib.urlencode({
                'language': language,
                'is_secure': request.is_secure(),
            }))
        footer_dict = cache.get(cache_key)
        if footer_dict is None:
            with translation.override(language):
                footer_dict = branding_api.get_footer(
                    is_secure=request.is_secure())
                cache.set(cache_key, footer_dict,
                          settings.FOOTER_CACHE_TIMEOUT)
        return JsonResponse(footer_dict,
                            200,
                            content_type="application/json; charset=utf-8")

    else:
        return HttpResponse(status=406)
Beispiel #30
0
def logout_user(request):
    logout(request)
    response = JsonResponse({"success": True})
    response.delete_cookie(settings.EDXMKTG_COOKIE_NAME,
        path='/', domain=settings.SESSION_COOKIE_DOMAIN,)
    return response
def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix,
                           user):
    """
    Invoke an XBlock handler, either authenticated or not.

    Arguments:
        request (HttpRequest): the current request
        course_id (str): A string of the form org/course/run
        usage_id (str): A string of the form i4x://org/course/category/name@revision
        handler (str): The name of the handler to invoke
        suffix (str): The suffix to pass to the handler when invoked
        user (User): The currently logged in user

    """
    try:
        course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
        usage_key = course_id.make_usage_key_from_deprecated_string(
            unquote_slashes(usage_id))
    except InvalidKeyError:
        raise Http404("Invalid location")

    # Check submitted files
    files = request.FILES or {}
    error_msg = _check_files_limits(files)
    if error_msg:
        return HttpResponse(json.dumps({'success': error_msg}))

    try:
        descriptor = modulestore().get_item(usage_key)
    except ItemNotFoundError:
        log.warn(
            "Invalid location for course id {course_id}: {usage_key}".format(
                course_id=usage_key.course_key, usage_key=usage_key))
        raise Http404

    tracking_context_name = 'module_callback_handler'
    tracking_context = {
        'module': {
            'display_name': descriptor.display_name_with_default,
        }
    }

    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
        course_id, user, descriptor)
    instance = get_module(user,
                          request,
                          usage_key,
                          field_data_cache,
                          grade_bucket_type='ajax')
    if instance is None:
        # Either permissions just changed, or someone is trying to be clever
        # and load something they shouldn't have access to.
        log.debug("No module %s for user %s -- access denied?", usage_key,
                  user)
        raise Http404

    req = django_to_webob_request(request)
    try:
        with tracker.get_tracker().context(tracking_context_name,
                                           tracking_context):
            resp = instance.handle(handler, req, suffix)

    except NoSuchHandlerError:
        log.exception("XBlock %s attempted to access missing handler %r",
                      instance, handler)
        raise Http404

    # If we can't find the module, respond with a 404
    except NotFoundError:
        log.exception("Module indicating to user that request doesn't exist")
        raise Http404

    # For XModule-specific errors, we log the error and respond with an error message
    except ProcessingError as err:
        log.warning("Module encountered an error while processing AJAX call",
                    exc_info=True)
        return JsonResponse(object={'success': err.args[0]}, status=200)

    # If any other error occurred, re-raise it to trigger a 500 response
    except Exception:
        log.exception("error executing xblock handler")
        raise

    return webob_to_django_response(resp)
Beispiel #32
0
def auto_auth(request):
    """
    Create or configure a user account, then log in as that user.

    Enabled only when
    settings.FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] is true.

    Accepts the following querystring parameters:
    * `username`, `email`, and `password` for the user account
    * `full_name` for the user profile (the user's full name; defaults to the username)
    * `staff`: Set to "true" to make the user global staff.
    * `course_id`: Enroll the student in the course with `course_id`
    * `roles`: Comma-separated list of roles to grant the student in the course with `course_id`
    * `no_login`: Define this to create the user but not login
    * `redirect`: Set to "true" will redirect to the `redirect_to` value if set, or
        course home page if course_id is defined, otherwise it will redirect to dashboard
    * `redirect_to`: will redirect to to this url
    * `is_active` : make/update account with status provided as 'is_active'
    If username, email, or password are not provided, use
    randomly generated credentials.
    """

    # Generate a unique name to use if none provided
    generated_username = uuid.uuid4().hex[0:30]
    generated_password = generate_password()

    # Use the params from the request, otherwise use these defaults
    username = request.GET.get('username', generated_username)
    password = request.GET.get('password', generated_password)
    email = request.GET.get('email', username + "@example.com")
    full_name = request.GET.get('full_name', username)
    is_staff = str2bool(request.GET.get('staff', False))
    is_superuser = str2bool(request.GET.get('superuser', False))
    course_id = request.GET.get('course_id')
    redirect_to = request.GET.get('redirect_to')
    is_active = str2bool(request.GET.get('is_active', True))

    # Valid modes: audit, credit, honor, no-id-professional, professional, verified
    enrollment_mode = request.GET.get('enrollment_mode', 'honor')

    # Parse roles, stripping whitespace, and filtering out empty strings
    roles = _clean_roles(request.GET.get('roles', '').split(','))
    course_access_roles = _clean_roles(request.GET.get('course_access_roles', '').split(','))

    redirect_when_done = str2bool(request.GET.get('redirect', '')) or redirect_to
    login_when_done = 'no_login' not in request.GET

    restricted = settings.FEATURES.get('RESTRICT_AUTOMATIC_AUTH', True)
    if is_superuser and restricted:
        return HttpResponseForbidden(_('Superuser creation not allowed'))

    form = AccountCreationForm(
        data={
            'username': username,
            'email': email,
            'password': password,
            'name': full_name,
        },
        tos_required=False
    )

    # Attempt to create the account.
    # If successful, this will return a tuple containing
    # the new user object.
    try:
        user, profile, reg = do_create_account(form)
    except (AccountValidationError, ValidationError):
        if restricted:
            return HttpResponseForbidden(_('Account modification not allowed.'))
        # Attempt to retrieve the existing user.
        user = User.objects.get(username=username)
        user.email = email
        user.set_password(password)
        user.is_active = is_active
        user.save()
        profile = UserProfile.objects.get(user=user)
        reg = Registration.objects.get(user=user)
    except PermissionDenied:
        return HttpResponseForbidden(_('Account creation not allowed.'))

    user.is_staff = is_staff
    user.is_superuser = is_superuser
    user.save()

    if is_active:
        reg.activate()
        reg.save()

    # ensure parental consent threshold is met
    year = datetime.date.today().year
    age_limit = settings.PARENTAL_CONSENT_AGE_LIMIT
    profile.year_of_birth = (year - age_limit) - 1
    profile.save()

    create_or_set_user_attribute_created_on_site(user, request.site)

    # Enroll the user in a course
    course_key = None
    if course_id:
        course_key = CourseLocator.from_string(course_id)
        CourseEnrollment.enroll(user, course_key, mode=enrollment_mode)

        # Apply the roles
        for role in roles:
            assign_role(course_key, user, role)

        for role in course_access_roles:
            CourseAccessRole.objects.update_or_create(user=user, course_id=course_key, org=course_key.org, role=role)

    # Log in as the user
    if login_when_done:
        user = authenticate_new_user(request, username, password)
        django_login(request, user)

    create_comments_service_user(user)

    if redirect_when_done:
        if redirect_to:
            # Redirect to page specified by the client
            redirect_url = redirect_to
        elif course_id:
            # Redirect to the course homepage (in LMS) or outline page (in Studio)
            try:
                redirect_url = reverse(course_home_url_name(course_key), kwargs={'course_id': course_id})
            except NoReverseMatch:
                redirect_url = reverse('course_handler', kwargs={'course_key_string': course_id})
        else:
            # Redirect to the learner dashboard (in LMS) or homepage (in Studio)
            try:
                redirect_url = reverse('dashboard')
            except NoReverseMatch:
                redirect_url = reverse('home')

        return redirect(redirect_url)
    else:
        response = JsonResponse({
            'created_status': 'Logged in' if login_when_done else 'Created',
            'username': username,
            'email': email,
            'password': password,
            'user_id': user.id,  # pylint: disable=no-member
            'anonymous_id': anonymous_id_for_user(user, None),
        })
    response.set_cookie('csrftoken', csrf(request)['csrf_token'])
    return response
Beispiel #33
0
def get_students_problem_grades(request, csv=False):
    """
    Get a list of students and grades for a particular problem.
    If 'csv' is False, returns a dict of student's name: username: grade: percent.

    If 'csv' is True, returns a header array, and an array of arrays in the format:
    student names, usernames, grades, percents for CSV download.
    """
    module_state_key = BlockUsageLocator.from_string(
        request.GET.get('module_id'))
    csv = request.GET.get('csv')

    # Query for "problem grades" students
    students = models.StudentModule.objects.select_related('student').filter(
        module_state_key=module_state_key,
        module_type__exact='problem',
        grade__isnull=False,
    ).values('student__username', 'student__profile__name', 'grade',
             'max_grade').order_by('student__profile__name')

    results = []
    if not csv:
        # Restrict screen list length
        # Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH
        # without doing another select.
        for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]:
            student_dict = {
                'name': student['student__profile__name'],
                'username': student['student__username'],
                'grade': student['grade'],
            }

            student_dict['percent'] = 0
            if student['max_grade'] > 0:
                student_dict['percent'] = round(student['grade'] * 100 /
                                                student['max_grade'])
            results.append(student_dict)

        max_exceeded = False
        if len(results) > MAX_SCREEN_LIST_LENGTH:
            # Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH
            del results[-1]
            max_exceeded = True

        response_payload = {
            'results': results,
            'max_exceeded': max_exceeded,
        }
        return JsonResponse(response_payload)
    else:
        tooltip = request.GET.get('tooltip')
        filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')])

        header = [_("Name"), _("Username"), _("Grade"), _("Percent")]
        for student in students:
            percent = 0
            if student['max_grade'] > 0:
                percent = round(student['grade'] * 100 / student['max_grade'])
            results.append([
                student['student__profile__name'],
                student['student__username'], student['grade'], percent
            ])

        response = create_csv_response(filename, header, results)
        return response