197 lines
6.0 KiB
Python
197 lines
6.0 KiB
Python
"""
|
|
Common code shared by course and library fixtures.
|
|
"""
|
|
import re
|
|
import requests
|
|
import json
|
|
from lazy import lazy
|
|
|
|
from common.test.acceptance.fixtures import STUDIO_BASE_URL
|
|
|
|
|
|
class StudioApiLoginError(Exception):
|
|
"""
|
|
Error occurred while logging in to the Studio API.
|
|
"""
|
|
pass
|
|
|
|
|
|
class StudioApiFixture(object):
|
|
"""
|
|
Base class for fixtures that use the Studio restful API.
|
|
"""
|
|
def __init__(self):
|
|
# Info about the auto-auth user used to create the course/library.
|
|
self.user = {}
|
|
|
|
@lazy
|
|
def session(self):
|
|
"""
|
|
Log in as a staff user, then return a `requests` `session` object for the logged in user.
|
|
Raises a `StudioApiLoginError` if the login fails.
|
|
"""
|
|
# Use auto-auth to retrieve the session for a logged in user
|
|
session = requests.Session()
|
|
response = session.get(STUDIO_BASE_URL + "/auto_auth?staff=true")
|
|
|
|
# Return the session from the request
|
|
if response.ok:
|
|
# auto_auth returns information about the newly created user
|
|
# capture this so it can be used by by the testcases.
|
|
user_pattern = re.compile(r'Logged in user {0} \({1}\) with password {2} and user_id {3}'.format(
|
|
r'(?P<username>\S+)', r'(?P<email>[^\)]+)', r'(?P<password>\S+)', r'(?P<user_id>\d+)'))
|
|
user_matches = re.match(user_pattern, response.text)
|
|
if user_matches:
|
|
self.user = user_matches.groupdict()
|
|
|
|
return session
|
|
|
|
else:
|
|
msg = "Could not log in to use Studio restful API. Status code: {0}".format(response.status_code)
|
|
raise StudioApiLoginError(msg)
|
|
|
|
@lazy
|
|
def session_cookies(self):
|
|
"""
|
|
Log in as a staff user, then return the cookies for the session (as a dict)
|
|
Raises a `StudioApiLoginError` if the login fails.
|
|
"""
|
|
return {key: val for key, val in self.session.cookies.items()}
|
|
|
|
@lazy
|
|
def headers(self):
|
|
"""
|
|
Default HTTP headers dict.
|
|
"""
|
|
return {
|
|
'Content-type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRFToken': self.session_cookies.get('csrftoken', '')
|
|
}
|
|
|
|
|
|
class FixtureError(Exception):
|
|
"""
|
|
Error occurred while installing a course or library fixture.
|
|
"""
|
|
pass
|
|
|
|
|
|
class XBlockContainerFixture(StudioApiFixture):
|
|
"""
|
|
Base class for course and library fixtures.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.children = []
|
|
super(XBlockContainerFixture, self).__init__()
|
|
|
|
def add_children(self, *args):
|
|
"""
|
|
Add children XBlock to the container.
|
|
Each item in `args` is an `XBlockFixtureDesc` object.
|
|
|
|
Returns the fixture to allow chaining.
|
|
"""
|
|
self.children.extend(args)
|
|
return self
|
|
|
|
def _create_xblock_children(self, parent_loc, xblock_descriptions):
|
|
"""
|
|
Recursively create XBlock children.
|
|
"""
|
|
for desc in xblock_descriptions:
|
|
loc = self.create_xblock(parent_loc, desc)
|
|
self._create_xblock_children(loc, desc.children)
|
|
|
|
def create_xblock(self, parent_loc, xblock_desc):
|
|
"""
|
|
Create an XBlock with `parent_loc` (the location of the parent block)
|
|
and `xblock_desc` (an `XBlockFixtureDesc` instance).
|
|
"""
|
|
create_payload = {
|
|
'category': xblock_desc.category,
|
|
'display_name': xblock_desc.display_name,
|
|
}
|
|
|
|
if parent_loc is not None:
|
|
create_payload['parent_locator'] = parent_loc
|
|
|
|
# Create the new XBlock
|
|
response = self.session.post(
|
|
STUDIO_BASE_URL + '/xblock/',
|
|
data=json.dumps(create_payload),
|
|
headers=self.headers,
|
|
)
|
|
|
|
if not response.ok:
|
|
msg = "Could not create {0}. Status was {1}".format(xblock_desc, response.status_code)
|
|
raise FixtureError(msg)
|
|
|
|
try:
|
|
loc = response.json().get('locator')
|
|
xblock_desc.locator = loc
|
|
except ValueError:
|
|
raise FixtureError("Could not decode JSON from '{0}'".format(response.content))
|
|
|
|
# Configure the XBlock
|
|
response = self.session.post(
|
|
STUDIO_BASE_URL + '/xblock/' + loc,
|
|
data=xblock_desc.serialize(),
|
|
headers=self.headers,
|
|
)
|
|
|
|
if response.ok:
|
|
return loc
|
|
else:
|
|
raise FixtureError("Could not update {0}. Status code: {1}".format(xblock_desc, response.status_code))
|
|
|
|
def _update_xblock(self, locator, data):
|
|
"""
|
|
Update the xblock at `locator`.
|
|
"""
|
|
# Create the new XBlock
|
|
response = self.session.put(
|
|
"{}/xblock/{}".format(STUDIO_BASE_URL, locator),
|
|
data=json.dumps(data),
|
|
headers=self.headers,
|
|
)
|
|
|
|
if not response.ok:
|
|
msg = "Could not update {} with data {}. Status was {}".format(locator, data, response.status_code)
|
|
raise FixtureError(msg)
|
|
|
|
def _encode_post_dict(self, post_dict):
|
|
"""
|
|
Encode `post_dict` (a dictionary) as UTF-8 encoded JSON.
|
|
"""
|
|
return json.dumps({
|
|
k: v.encode('utf-8') if isinstance(v, basestring) else v
|
|
for k, v in post_dict.items()
|
|
})
|
|
|
|
def get_nested_xblocks(self, category=None):
|
|
"""
|
|
Return a list of nested XBlocks for the container that can be filtered by
|
|
category.
|
|
"""
|
|
xblocks = self._get_nested_xblocks(self)
|
|
if category:
|
|
xblocks = [x for x in xblocks if x.category == category]
|
|
return xblocks
|
|
|
|
def _get_nested_xblocks(self, xblock_descriptor):
|
|
"""
|
|
Return a list of nested XBlocks for the container.
|
|
"""
|
|
xblocks = list(xblock_descriptor.children)
|
|
for child in xblock_descriptor.children:
|
|
xblocks.extend(self._get_nested_xblocks(child))
|
|
return xblocks
|
|
|
|
def _publish_xblock(self, locator):
|
|
"""
|
|
Publish the xblock at `locator`.
|
|
"""
|
|
self._update_xblock(locator, {'publish': 'make_public'})
|