class CleanableCM(object): """Cleanable context manager (based on ExitStack)""" def __init__(self): super(CleanableCM, self).__init__() self.stack = ExitStack() def _enter(self): """Should be override""" raise NotImplementedError @contextmanager def _cleanup_on_error(self): with ExitStack() as stack: stack.push(self) yield stack.pop_all() def __enter__(self): with self._cleanup_on_error(): self.stack.__enter__() return self._enter() def __exit__(self, exc_type, exc_value, traceback): self.stack.__exit__(exc_type, exc_value, traceback)
class LoggedExitStack(object): def __init__(self, logger, context_managers=None): self._logger = logger self._exit_stack = ExitStack() # TODO make this cleaner, types would be nice context_managers = context_managers if context_managers is not None else [] self.context_managers = (context_managers if isinstance( context_managers, list) else [context_managers]) def __enter__(self): self._exit_stack.__enter__() for context_manager in self.context_managers: self._exit_stack.enter_context( LogScope(self._logger, context_manager.__class__.__name__)) self._exit_stack.enter_context(context_manager) return self def __exit__(self, *args): return self._exit_stack.__exit__(*args)
class _TestUser(object): def __init__(self, test_client, runestone_db_tools, username, password, course_name): self.test_client = test_client self.runestone_db_tools = runestone_db_tools self.username = username self.first_name = 'test' self.last_name = 'user' self.email = self.username + '@foo.com' self.password = password self.course_name = course_name def __enter__(self): # Registration doesn't work unless we're logged out. self.test_client.logout() # Now, post the registration. self.test_client.validate( 'default/user/register', 'Course Selection', data=dict( username=self.username, first_name=self.first_name, last_name=self.last_name, # The e-mail address must be unique. email=self.email, password=self.password, password_two=self.password, # Note that ``course_id`` is (on the form) actually a course name. course_id=self.course_name, accept_tcp='on', donate='0', _next='/runestone/default/index', _formname='register', )) # Schedule this user for deletion. self.exit_stack_object = ExitStack() self.exit_stack = self.exit_stack_object.__enter__() self.exit_stack.callback(self._delete_user) # Record IDs db = self.runestone_db_tools.db self.course_id = db(db.courses.course_name == self.course_name).select( db.courses.id).first().id self.user_id = db(db.auth_user.username == self.username).select( db.auth_user.id).first().id return self def __exit__(self, exc_type, exc_value, traceback): self.exit_stack_object.__exit__(exc_type, exc_value, traceback) # Delete the user created by entering this context manager. def _delete_user(self): db = self.runestone_db_tools.db # Delete the course this user registered for. db((db.user_courses.course_id == self.course_id) & (db.user_courses.user_id == self.user_id)).delete() # Delete the user. db(db.auth_user.username == self.username).delete() db.commit() def login(self): self.test_client.post('default/user/login', data=dict( username=self.username, password=self.password, _formname='login', )) def make_instructor(self, course_id=None): # If ``course_id`` isn't specified, use this user's ``course_id``. course_id = course_id or self.course_id return self.runestone_db_tools.make_instructor(self.user_id, course_id) def add_user_to_course(self, course_id=None): # If ``course_id`` isn't specified, use this user's ``course_id``. course_id = course_id or self.course_id return self.runestone_db_tools.add_user_to_course( self.user_id, course_id)
class _TestUser(object): def __init__( self, test_client, runestone_db_tools, username, password, course_name, # True if the course is free (no payment required); False otherwise. is_free=True): self.test_client = test_client self.runestone_db_tools = runestone_db_tools self.username = username self.first_name = 'test' self.last_name = 'user' self.email = self.username + '@foo.com' self.password = password self.course_name = course_name self.is_free = is_free def __enter__(self): # Registration doesn't work unless we're logged out. self.test_client.logout() # Now, post the registration. self.test_client.validate( 'default/user/register', 'Support Runestone Interactive' if self.is_free else 'Payment Amount', data=dict( username=self.username, first_name=self.first_name, last_name=self.last_name, # The e-mail address must be unique. email=self.email, password=self.password, password_two=self.password, # Note that ``course_id`` is (on the form) actually a course name. course_id=self.course_name, accept_tcp='on', donate='0', _next='/runestone/default/index', _formname='register', )) # Schedule this user for deletion. self.exit_stack_object = ExitStack() self.exit_stack = self.exit_stack_object.__enter__() self.exit_stack.callback(self._delete_user) # Record IDs db = self.runestone_db_tools.db self.course_id = db(db.courses.course_name == self.course_name).select( db.courses.id).first().id self.user_id = db(db.auth_user.username == self.username).select( db.auth_user.id).first().id return self # Clean up on exit by invoking all ``__exit__`` methods. def __exit__(self, exc_type, exc_value, traceback): self.exit_stack_object.__exit__(exc_type, exc_value, traceback) # Delete the user created by entering this context manager. TODO: This doesn't delete all the chapter progress tracking stuff. def _delete_user(self): db = self.runestone_db_tools.db # Delete the course this user registered for. db((db.user_courses.course_id == self.course_id) & (db.user_courses.user_id == self.user_id)).delete() # Delete the user. db(db.auth_user.username == self.username).delete() db.commit() def login(self): self.test_client.post('default/user/login', data=dict( username=self.username, password=self.password, _formname='login', )) def make_instructor(self, course_id=None): # If ``course_id`` isn't specified, use this user's ``course_id``. course_id = course_id or self.course_id return self.runestone_db_tools.make_instructor(self.user_id, course_id) # A context manager to update this user's profile. If a course was added, it returns that course's ID; otherwise, it returns None. @contextmanager def update_profile( self, # This parameter is passed to ``test_clint.validate``. expected_string=None, # An updated username, or ``None`` to use ``self.username``. username=None, # An updated first name, or ``None`` to use ``self.first_name``. first_name=None, # An updated last name, or ``None`` to use ``self.last_name``. last_name=None, # An updated email, or ``None`` to use ``self.email``. email=None, # An updated last name, or ``None`` to use ``self.course_name``. course_name=None, section='', # A shortcut for specifying the ``expected_string``, which only applies if ``expected_string`` is not set. Use ``None`` if a course will not be added, ``True`` if the added course is free, or ``False`` if the added course is paid. is_free=None, # The value of the ``accept_tcp`` checkbox; provide an empty string to leave unchecked. The default value leaves it checked. accept_tcp='on'): if expected_string is None: if is_free is None: expected_string = 'Course Selection' else: expected_string = 'Support Runestone Interactive' \ if is_free else 'Payment Amount' username = username or self.username first_name = first_name or self.first_name last_name = last_name or self.last_name email = email or self.email course_name = course_name or self.course_name db = self.runestone_db_tools.db # Determine if we're adding a course. If so, delete it at the end of the test. To determine if a course is being added, the course must exist, but not be in the user's list of courses. course = db(db.courses.course_name == course_name).select( db.courses.id).first() delete_at_end = course and not db( (db.user_courses.user_id == self.user_id) & (db.user_courses.course_id == course.id)).select( db.user_courses.id).first() # Perform the update. try: self.test_client.validate( 'default/user/profile', expected_string, data=dict( username=username, first_name=first_name, last_name=last_name, email=email, # Though the field is ``course_id``, it's really the course name. course_id=course_name, accept_tcp=accept_tcp, section=section, _next='/runestone/default/index', id=str(self.user_id), _formname='auth_user/' + str(self.user_id), )) yield course.id if delete_at_end else None finally: if delete_at_end: db = self.runestone_db_tools.db db((db.user_courses.user_id == self.user_id) & (db.user_courses.course_id == course.id)).delete() db.commit() # Call this after registering for a new course or adding a new course via ``update_profile`` to pay for the course. @contextmanager def make_payment( self, # The `Stripe test tokens <https://stripe.com/docs/testing#cards>`_ to use for payment. stripe_token, # The course ID of the course to pay for. None specifies ``self.course_id``. course_id=None): course_id = course_id or self.course_id # Get the signature from the HTML of the payment page. self.test_client.validate('default/payment') match = re.search( '<input type="hidden" name="signature" value="([^ ]*)" \/>', self.test_client.text) signature = match.group(1) try: self.test_client.validate( 'default/payment', ['Thank you for your payment', 'Payment failed'], data=dict(stripeToken=stripe_token, signature=signature)) yield None finally: db = self.runestone_db_tools.db db((db.user_courses.course_id == course_id) & (db.user_courses.user_id == self.user_id)).delete()