def previews_callback():
    if not verify_preview_service_authorization(
            request.headers.get('authorization')):
        raise UnauthorizedRequestError(
            'Missing or invalid authorization header.')

    params = request.form
    if not (params.get('id', None) and params.get('status', None)):
        raise BadRequestError('Id and status fields required.')
    metadata = None
    try:
        if params.get('metadata'):
            metadata = json.loads(params['metadata'])
    except Exception as e:
        app.logger.error('Failed to parse JSON preview metadata.')
        app.logger.exception(e)
        raise BadRequestError('Could not parse JSON metadata.')

    asset = Asset.find_by_id(params['id'])
    if not asset:
        raise BadRequestError(f"Asset {params['id']} not found.")

    if asset.update_preview(
            preview_status=params.get('status'),
            thumbnail_url=params.get('thumbnail'),
            image_url=params.get('image'),
            pdf_url=params.get('pdf'),
            metadata=metadata,
    ):
        return tolerant_jsonify({'status': 'success'})
    else:
        raise InternalServerError(
            f"Unable to update preview data (asset_id={params['id']}.")
Exemple #2
0
def _get_upload_from_http_post():
    request_files = request.files
    file = request_files.get('file[0]')
    if not file:
        raise BadRequestError('request.files is empty')

    filename = file.filename and file.filename.strip()
    if not filename:
        raise BadRequestError(f'Invalid file: {filename}')

    return {
        'name': filename.rsplit('/', 1)[-1],
        'byte_stream': file.read(),
    }
Exemple #3
0
def _get_asset_for_like(asset_id):
    asset = Asset.find_by_id(asset_id=asset_id)
    if not asset or not can_view_asset(asset=asset, user=current_user):
        raise ResourceNotFoundError(f'No asset found with id: {asset_id}')
    elif current_user.user in asset.users:
        raise BadRequestError('You cannot like your own asset.')
    else:
        return asset
Exemple #4
0
def delete_asset(asset_id):
    asset = Asset.find_by_id(asset_id) if asset_id else None
    if not asset:
        raise ResourceNotFoundError('Asset not found.')
    if not can_update_asset(asset=asset, user=current_user):
        raise BadRequestError(
            'To delete this asset you must own it or be a teacher in the course.'
        )
    Asset.delete(asset_id=asset_id)
    return tolerant_jsonify({'message': f'Asset {asset_id} deleted'}), 200
def create_category():
    params = request.get_json() or request.form
    title = params.get('title')
    if not title:
        raise BadRequestError('Category creation requires title.')
    category = Category.create(
        canvas_assignment_name=title,
        course_id=current_user.course.id,
        title=title,
    )
    return tolerant_jsonify(category.to_api_json())
Exemple #6
0
def update_asset():
    params = request.get_json()
    asset_id = params.get('assetId')
    category_id = params.get('categoryId')
    description = params.get('description')
    title = params.get('title')
    asset = Asset.find_by_id(asset_id) if asset_id else None
    if not asset or not title:
        raise BadRequestError('Asset update requires a valid ID and title.')
    if not can_update_asset(asset=asset, user=current_user):
        raise BadRequestError(
            'To update an asset you must own it or be a teacher in the course.'
        )
    asset = Asset.update(
        asset_id=asset_id,
        categories=category_id and [Category.find_by_id(category_id)],
        description=description,
        title=title,
    )
    return tolerant_jsonify(asset.to_api_json(user_id=current_user.get_id()))
Exemple #7
0
def create_asset():
    params = request.get_json() or request.form
    asset_type = params.get('type')
    category_id = params.get('categoryId')
    description = params.get('description')
    source = params.get('source')
    url = params.get('url')
    title = params.get('title', url)
    visible = params.get('visible', True)
    if not asset_type or not title:
        raise BadRequestError('Asset creation requires title and type.')

    if asset_type == 'link' and not url:
        raise BadRequestError('Link asset creation requires url.')

    if not current_user.course:
        raise BadRequestError('Course data not found')

    s3_attrs = {}
    if asset_type == 'file':
        file_upload = _get_upload_from_http_post()
        s3_attrs = Asset.upload_to_s3(
            filename=file_upload['name'],
            byte_stream=file_upload['byte_stream'],
            course_id=current_user.course.id,
        )

    asset = Asset.create(
        asset_type=asset_type,
        categories=category_id and [Category.find_by_id(category_id)],
        course_id=current_user.course.id,
        description=description,
        download_url=s3_attrs.get('download_url', None),
        mime=s3_attrs.get('content_type', None),
        source=source,
        title=title,
        url=url,
        users=[User.find_by_id(current_user.get_id())],
        visible=visible,
    )
    return tolerant_jsonify(asset.to_api_json())
Exemple #8
0
def update_comment(comment_id):
    params = request.get_json()
    comment = Comment.find_by_id(comment_id=comment_id)
    if comment and can_update_comment(comment=comment, user=current_user):
        body = params.get('body', '').strip()
        if not body:
            raise BadRequestError('Comment body is required.')
        comment = Comment.update(body=body, comment_id=comment.id)
        return tolerant_jsonify(_decorate_comments([comment.to_api_json()])[0])
    else:
        raise ResourceNotFoundError(
            'Asset is either unavailable or non-existent.')
def update_category():
    params = request.get_json()
    category_id = params.get('categoryId')
    title = params.get('title')
    visible = to_bool_or_none(params.get('visible'))
    category = Category.find_by_id(category_id) if category_id else None
    if not category or not title:
        raise BadRequestError('Category update requires categoryId and title.')
    category = Category.update(
        category_id=category_id,
        title=title,
        visible=category.visible if visible is None else visible,
    )
    return tolerant_jsonify(category.to_api_json())
Exemple #10
0
def create_comment():
    params = request.get_json()
    asset_id = params.get('assetId')
    asset = Asset.find_by_id(asset_id=asset_id)
    if asset and can_view_asset(asset=asset, user=current_user):
        body = params.get('body', '').strip()
        if not body:
            raise BadRequestError('Comment body is required.')
        parent_id = params.get('parentId')
        comment = Comment.create(
            asset=asset,
            user_id=current_user.user_id,
            body=body,
            parent_id=parent_id and int(parent_id),
        )
        return tolerant_jsonify(_decorate_comments([comment.to_api_json()])[0])
    else:
        raise ResourceNotFoundError(
            'Asset is either unavailable or non-existent.')
def _lti_launch_authentication(tool_id):
    is_asset_library = tool_id == TOOL_ID_ASSET_LIBRARY
    is_engagement_index = tool_id == TOOL_ID_ENGAGEMENT_INDEX
    if not is_asset_library and not is_engagement_index:
        raise BadRequestError(f'Missing or invalid tool_id: {tool_id}')

    def _alpha_num(s):
        value = _str_strip(s)
        return value if value.isalnum() else None

    args = request.form
    lti_params = {}
    validation = {
        'custom_canvas_api_domain': _str_strip,
        'custom_canvas_course_id': _str_strip,
        'custom_canvas_user_id': _str_strip,
        'custom_external_tool_url': _canvas_external_tool_url,
        'lis_person_name_full': _str_strip,
        'oauth_consumer_key': _alpha_num,
        'oauth_nonce': _alpha_num,
        'oauth_signature_method': _str_strip,
        'oauth_timestamp': _str_strip,
        'oauth_version': _str_strip,
        'roles': _str_strip,
    }

    def _fetch(key):
        value = args.get(key)
        validate = validation[key]
        pass_headers = validate is _canvas_external_tool_url
        validated_value = validate(value, request.headers) if pass_headers else validate(value)
        if validated_value:
            lti_params[key] = validated_value
            return lti_params[key]
        else:
            app.logger.warning(f'Invalid \'{key}\' parameter in LTI launch: {value}')

    if all(_fetch(key) for key in validation.keys()):
        app.logger.info(f'LTI launch params passed basic validation: {lti_params}')
        canvas_api_domain = lti_params['custom_canvas_api_domain']
        canvas = Canvas.find_by_domain(canvas_api_domain)

        if not canvas:
            raise ResourceNotFoundError(f'Failed \'canvas\' lookup where canvas_api_domain = {canvas_api_domain}')

        if canvas.lti_key != lti_params['oauth_consumer_key']:
            raise BadRequestError(f'oauth_consumer_key does not match {canvas_api_domain} lti_key in squiggy db.')

        tool_provider = FlaskToolProvider.from_flask_request(
            request=request,
            secret=canvas.lti_secret,
        )
        valid_request = tool_provider.is_valid_request(LtiRequestValidator(canvas))

        if valid_request or app.config['TESTING']:
            # TODO: We do not want app.config['TESTING'] in this conditional. It is here because our tests are failing
            #       on HMAC-signature verification. Let's fix after we get a successful LTI launch.
            app.logger.info(f'FlaskToolProvider validated {canvas_api_domain} LTI launch request.')
        else:
            raise BadRequestError(f'LTI oauth failed in {canvas_api_domain} request')

        external_tool_url = lti_params['custom_external_tool_url']
        canvas_course_id = lti_params['custom_canvas_course_id']
        course = Course.find_by_canvas_course_id(
            canvas_api_domain=canvas_api_domain,
            canvas_course_id=canvas_course_id,
        )
        if course:
            course = Course.update(
                asset_library_url=external_tool_url if is_asset_library else course.asset_library_url,
                course_id=course.id,
                engagement_index_url=external_tool_url if is_engagement_index else course.engagement_index_url,
            )
            app.logger.info(f'Updated course during LTI launch: {course.to_api_json()}')
        else:
            course = Course.create(
                asset_library_url=external_tool_url if is_asset_library else None,
                canvas_api_domain=canvas_api_domain,
                canvas_course_id=canvas_course_id,
                engagement_index_url=external_tool_url if is_engagement_index else None,
                name=args.get('context_title'),
            )
            app.logger.info(f'Created course via LTI launch: {course.to_api_json()}')

        canvas_user_id = lti_params['custom_canvas_user_id']
        user = User.find_by_course_id(canvas_user_id=canvas_user_id, course_id=course.id)
        if user:
            app.logger.info(f'Found user during LTI launch: canvas_user_id={canvas_user_id}, course_id={course.id}')
        else:
            user = User.create(
                course_id=course.id,
                canvas_user_id=canvas_user_id,
                canvas_course_role=str(lti_params['roles']),
                canvas_enrollment_state=args.get('custom_canvas_enrollment_state') or 'active',
                canvas_full_name=lti_params['lis_person_name_full'],
                canvas_image=args.get('user_image'),  # TODO: Verify user_image.
                canvas_email=args.get('lis_person_contact_email_primary'),
                canvas_course_sections=None,  # TODO: Set by poller?
            )
            app.logger.info(f'Created user during LTI launch: canvas_user_id={canvas_user_id}')

        path = '/assets' if is_asset_library else '/engage'
        params = f'canvasApiDomain={canvas_api_domain}&canvasCourseId={canvas_course_id}'
        app.logger.info(f'LTI launch redirect: {path}?{params}')
        return user, f'{path}?{params}'