""" 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'})