def questions(self) -> List[QuestionBase]: """Access the list of questions. :return: the list of questions within the multi-part assessment :rtype: list(:py:class:`~regions.tutor.assessment.QuestionBase`) :raises :py:class:`~utils.tutor.TutorException`: if a task step doesn't match a free response or multiple choice question or if no assessment segments are found """ parts = [] for question in self.find_elements(*self._question_locator): if question.find_elements(*self._is_free_response_locator): parts.append(FreeResponse(self, question)) elif question.find_elements(*self._is_multiple_choice_locator): parts.append(MultipleChoice(self, question)) else: tag = question.get_attribute('data-task-step-id') raise TutorException( f'Unknown assessment type in task step {tag}') if not parts: raise TutorException('No multi-part steps found in "{0}"'.format( self.driver.current_url)) return parts
def select_assignment(self, name: str, _type: str = None): """Click on an assignment. :param str name: the assignment's name :param str _type: (optional) the assignment's type :return: the assignment task page(s) :rtype: :py:class:`~pages.tutor.task.Assignment` """ for assignment in self.find_elements(*self._assignment_link_locator): if name in assignment.get_attribute('textContent'): description = assignment.get_attribute('class') if _type and _type not in description: continue if Tutor.EVENT in description: from pages.tutor.task import Event as Destination elif Tutor.EXTERNAL in description: from pages.tutor.task import EXTERNAL as Destination elif Tutor.HOMEWORK in description: from pages.tutor.task import Homework as Destination elif Tutor.READING in description: from pages.tutor.task import Reading as Destination else: raise TutorException( f'Unknown assignment type in "{description}"') Utility.click_option(self.driver, element=assignment) sleep(1) return go_to_(Destination(self.driver, self.base_url)) raise TutorException(f'"{name}" not found in the currently ' + 'available assignments')
def get_instructor(self, first_name=None, last_name=None, position=None): """Return the instructor row based on their name or position. :param str first_name: (optional) match the instructor row by the first match on the teacher's first name :param str last_name: (optional) match the instructor row by the first match on the teacher's last name :param int position: (optional) select the instructor row by the row position (e.g. ``position = 1`` is the first instructor) :return: the instructor row :rtype: :py:class:`~CourseRoster.Teachers.Teacher` :raises TutorException: if a match isn't found """ if position: return self.instructors[position - 1] elif first_name: for instructor in self.instructors: if instructor.first_name == first_name: return instructor raise TutorException( 'No instructor matches first name "{0}"'.format( first_name)) elif last_name: for instructor in self.instructors: if instructor.last_name == last_name: return instructor raise TutorException( 'No instructor matches last name "{0}"'.format(last_name)) raise TutorException('No match option provided')
def student_access(self): """Access the student payment information.""" try: root = self.find_element(*self._student_payment_locator) return self.Payment(self, root) except WebDriverException: raise TutorException('No active trial')
def purchase(self) -> Union[PurchaseConfirmation]: """Click on the 'Purchase' Tutor button. :return: the purchase confirmation modal or the error message for a failed transaction :rtype: :py:class:`~pages.tutor.enrollment.PurchaseConfirmation` :raises: :py:class:`~utils.tutor.TutorException` if error messages are found """ purchase = self.find_element(*self._base_iframe_locator) self.driver.switch_to.frame(purchase) button = self.wait.until( expect.presence_of_element_located(self._purchase_button_locator)) sleep(0.25) is_not_chrome = not Utility.is_browser(self.driver, 'chrome') Utility.click_option(self.driver, element=button, force_js_click=is_not_chrome) sleep(1) self.driver.switch_to.default_content() errors = self.error_messages if errors: raise TutorException(errors) return PurchaseConfirmation(self.page)
def go_to_course(self, name: str): """Go to a specific course. :param str name: the course name to select :return: the calendar (teacher) or current week (student) page for the specific course :rtype: :py:class:`~pages.tutor.calendar.Calendar` or :py:class:`~pages.tutor.course.StudentCourse` :raises :py:class:`~utils.tutor.TutorException`: if no previous course or current course exists or if a course does not match ``name`` """ if self.is_safari: sleep(2) self.wait.until(lambda _: bool(self.current_courses.courses)) # Look through current courses first try: for course in self.current_courses.courses: if course.title == name: return course.go_to_course() except AssertionError: pass # Then try previous courses try: for course in self.past_courses.courses: if course.title == name: return course.go_to_course() except AssertionError: pass raise TutorException(f'No course found matching "{name}"')
def _add_assignment(self, assignment_type): """Click on an add assignment button. An internal helper function to select a new assignment. :param str assignment_type: the type of assignment to add :return: an assignment creation page :rtype: :py:class:`~pages.tutor.assignment.AddExternal` or :py:class:`~pages.tutor.assignment.AddEvent` or :py:class:`~pages.tutor.assignment.AddHomework` or :py:class:`~pages.tutor.assignment.AddReading` :raises :py:class:`~utils.tutor.TutorException`: if the assignment_type does not match a known assignment type :noindex: """ if assignment_type == Tutor.EXTERNAL: locator = self._add_external_locator from pages.tutor.assignment import External as Assignment elif assignment_type == Tutor.EVENT: locator = self._add_event_locator from pages.tutor.assignment import Event as Assignment elif assignment_type == Tutor.HOMEWORK: locator = self._add_homework_locator from pages.tutor.assignment import Homework as Assignment elif assignment_type == Tutor.READING: locator = self._add_reading_locator from pages.tutor.assignment import Reading as Assignment else: raise TutorException('"{0}" is not a known assignment type.' .format(assignment_type)) button = self.find_element(*locator) Utility.click_option(self.driver, element=button) return go_to_(Assignment(self.driver, self.page.base_url))
def chat_with_support(self): """Begin a customer support chat session.""" if self.chat_available: from pages.tutor.chat import Chat self._select(locator=self._chat_support_enabled_locator, destination=Chat, external=True) raise TutorException('Chat is not currently available')
def performance(self): """Access the performance bars for homeworks and readings.""" if self.page.assignment_type == Tutor.EXTERNAL: return TutorException( 'External assignments do not have performance bars.') return [ self.Performance(self, bar) for bar in self.find_elements(*self._performance_locator) ]
def students(self, students: int = 1) -> None: """Set the estimated number of students who will enroll. :param int students: the estimated number of students who will enroll in the course :return: None :raises :py:class:`~utils.tutor.TutorException`: if the number of students is less than 1 or greater than 1,500 """ if students < 1: raise TutorException( f'the minimum student estimate is 1 ({students} < 1)') self.find_element(*self._student_estimate_locator) \ .send_keys(students) sleep(0.25) error = self.error if error: raise TutorException(error.strip())
def sections(self, sections: int = 1) -> None: """Set the initial number of course sections or periods to create. :param int sections: the requested number of sections to create for the new course :return: None :raises :py:class:`~utils.tutor.TutorException`: if the number of sections is less than 1 or greater than 10 """ if sections < 1: raise TutorException( f'the minimum number of sections is 1 ({sections} < 1)') section_box = self.find_element(*self._course_sections_locator) self.driver.execute_script('arguments[0].value = "";', section_box) section_box.send_keys(sections) sleep(0.25) error = self.error if error: raise TutorException(error.strip())
def get_course_tile(self, name: str) -> Courses.Course: """Return a course tile by matching the course name. :param str name: the course name to match :return: the course picker course tile :rtype: :py:class:`~pages.tutor.dashboard.Courses.Course` """ for course in self.courses: if course.title == name: return course raise TutorException(f'No course found matching "{name}"')
def select_section(self, name=None, position=1): """Select a section or period tab by name or position. .. note:: section/period names are case sensitive (e.g. '1st' and '1ST' can occur in the same course) :param str name: (optional) the section or period name to select :param int position: (optional) the position of the section or period tab to select from 1 to the number of tabs :return: the course roster displaying the selected course section roster :rtype: :py:class:`CourseRoster` :raises :py:class:`~utils.tutor.TutorException`: if the name does not match an existing tab name or if the position is not between 1 and number of active tabs """ if name: section_found = False for section in self.sections: if section.name == name: section_found = True section.select() if not section_found: raise TutorException( '"{name}" does not match any active section'.format( name=name)) else: maximum = len(self.sections) if position < 1 or position > maximum: raise TutorException( "position {pos} is not between 1 and {max}".format( position, maximum)) self.sections[position - 1].select() sleep(0.5) return self.page
def random_answer(self) -> None: """Select a random answer for the question. :return: None :raises: :py:class:`~utils.tutor.TutorException` if no multiple choice answers are available """ sleep(0.25) answers = self.answers if not answers: raise TutorException('No answers found') answers[Utility.random(0, len(answers) - 1)].select() sleep(0.25)
def sections(self): """Access the section forecasts. :return: the list of sections in the recent forecast :rtype: list(:py:class:`~StudentCourse.Performance.Section`) :raises :py:class:`~utils.tutor.TutorException`: if the recent topics forecast is empty """ if self.is_empty: raise TutorException("Forecast is not populated") return [self.Section(self, section) for section in self.find_elements(*self._section_locator)]
def select_by_name(self, name: str) -> None: """Select a course to clone by name. :param str name: the existing course name :return: None :raises :py:class:`~utils.tutor.TutorException`: if the name is not found or is not available """ for option in self.courses: if option.name.lower() == name.lower(): option.select() return raise TutorException(f'"{name}" not found or not available')
def forecast(self): """Access the performance forecast rows. :return: the forecast panels :rtype: :py:class:`~PerformanceForecast.Guide` :raise :py:class:`~utils.tutor.TutorException`: if the guide is not found """ try: forecast = self.find_element(*self._group_forecast_locator) return self.Guide(self, forecast) except NoSuchElementException: raise TutorException('Performance guide not found; is there data?')
def view_section(self, name): """View a course section by its name. :return: the performance forecast for the requested section or period :rtype: :py:class:`PerformanceForecast` :raises :py:class:`utils.tutor.TutorException`: if the name doesn't match an available course section """ for tab in self.section_tabs: if tab.name == name: tab.select() return self.page raise TutorException('"{0}" does not match any active section')
def select_by_title(self, title: str) -> None: """Select a course book by the book title. :param str title: the book title :return: None :raises :py:class:`~utils.tutor.TutorException`: if the title is not found or is not available """ for option in self.books: if option.title.lower() == title.lower(): option.select() return raise TutorException(f'"{title}" not found or not available')
def select_by_term(self, term: str) -> None: """Select a term by the semester or quarter name. :param str term: the term to select :return: None :raises :py:class:`~utils.tutor.TutorException`: if the term is not found or is not available """ for option in self.terms: if option.term.lower() == term.lower(): option.select() return raise TutorException(f'"{term}" not found or not available')
def assignment(self, name: str) -> Assignment: """Return the assignment from its name. :param str name: the assignment name or title :return: the assignment :rtype: :py:class:`~pages.tutor.calendar.Assignment` :raises :py:class:`~utils.tutor.TutorException` if an assignment does not match ``name`` """ for assignment in self.assignments(): if assignment.title == name: return assignment raise TutorException(f'No assignment found matching "{name}"')
def add_assignment(self, assignment_type=None): """Click on the date number to add an assignment. :param str assignment_type: the type of assignment to add :return: an assignment creation page :rtype: :py:class:`~pages.tutor.assignment.AddExternal` or :py:class:`~pages.tutor.assignment.AddEvent` or :py:class:`~pages.tutor.assignment.AddHomework` or :py:class:`~pages.tutor.assignment.AddReading` :raises :py:class:`~utils.tutor.TutorException`: if the assignment_type does not match a known assignment type """ label = self.find_element(*self._date_label_locator) Utility.click_option(self.driver, element=label) sleep(0.5) if assignment_type == Tutor.EVENT: assignment = Tutor.EVENT from pages.tutor.assignment import Event \ as Assignment elif assignment_type == Tutor.EXTERNAL: assignment = Tutor.EXTERNAL from pages.tutor.assignment import External \ as Assignment elif assignment_type == Tutor.HOMEWORK: assignment = Tutor.HOMEWORK from pages.tutor.assignment import Homework \ as Assignment elif assignment_type == Tutor.READING: assignment = Tutor.READING from pages.tutor.assignment import Reading \ as Assignment elif assignment_type is None: # just open the pop up menu return self.page.page else: raise TutorException( '"{0}" is not a known assignment type.' .format(assignment_type)) if assignment: button = self.driver.execute_script( 'return document.querySelector("{}");'.format( self._add_assignment_selector.format(assignment))) Utility.click_option(self.driver, element=button) return go_to_(Assignment(self.driver, self.page.page.base_url))
def select_timezone(self, timezone=Tutor.CENTRAL_TIME): """Select a timezone from the available radio options. :param str timezone: (optional) the new timezone, defaults to :py:data:`~utils.tutor.Tutor.CENTRAL_TIME` :return: the timezone modal :rtype: :py:class:`ChangeCourseTimezone` :raise :py:class:`utils.tutor.TutorException`: if the timezone does not match an available option """ for zone in self.timezones: if zone.name == timezone: zone.select() return self raise TutorException('"{0}" is not a known timezone'.format(timezone))
def answer(self) -> Practice: """Submit the answer. :return: the next step in the practice session :rtype: :py:class:`~pages.tutor.practice.Practice` :raises :py:class:`~utils.tutor.TutorException`: if the answer button is disabled """ if not self.answer_enabled: raise TutorException("The answer button is currently disabled; " "next step not available") button = self.find_element(*self._answer_button_locator) Utility.click_option(self.driver, element=button) sleep(1) return self
def term(self): """Return whether the date is in or out of the term. :return: the status of a date with respect to the course term :rtype: str """ term = self.root.get_attribute('class') if Tutor.BEFORE_TERM in term: return Tutor.BEFORE_TERM elif Tutor.IN_TERM in term: return Tutor.IN_TERM elif Tutor.AFTER_TERM in term: return Tutor.AFTER_TERM else: raise TutorException('No term-status listed in "{0}"' .format(term.split()))
def delete(self): """Click on the 'Delete' button confirming the section removal. :return: the course roster :rtype: :py:class:`CourseRoster` :raises: :py:class:`~utils.tutor.TutorException` when the course period/section cannot be deleted """ button = self.find_element(*self._delete_button_locator) Utility.click_option(self.driver, element=button) sleep(0.25) try: self.wait.until(expect.staleness_of(self.root)) except TimeoutException: raise TutorException('Could not delete the course section') sleep(1) return self.page
def name(self, name): """Change the course name. :param str name: the course's new name or title :return: the course rename modal :rtype: :py:class:`RenameCourse` :raises :py:class:`~utils.tutor.TutorException`: if name is not a valid course name (``bool(name) == False``) """ if not name: raise TutorException('"{0}" is not a valid course name' .format(name)) field = self.find_element(*self._course_name_input_locator) Utility.clear_field(self.driver, field=field) sleep(0.25) field.send_keys(name) sleep(0.75) return self
def create_a_course(self): """Select the create course tile. :return: the course creation wizard :rtype: :py:class:`~pages.tutor.new_course.NewCourse` """ try: self.wait.until( lambda _: self.find_elements(*self._create_tile_locator)) except TimeoutException: raise TutorException("Create a course tile not found; " "check user's faculty verification") tile = self.find_elements(*self._create_tile_locator) link = tile[0].find_element(By.CSS_SELECTOR, 'a') Utility.scroll_to(self.driver, element=tile[0], shift=-80) Utility.click_option(self.driver, element=link) from pages.tutor.new_course import NewCourse return go_to_(NewCourse(self.driver, self.base_url))
def state(self, state: str) -> None: """Set the district, state, or territory. :param str state: the state's select menu label for the district, state, or U.S. territory :return: None :raises :py:class:`~utils.tutor.TutorException`: if the state is not a valid state label """ if state not in Tutor.states(): raise TutorException( '"{0}" not a valid state; refer to '.format(state) + 'utils.tutor.States for valid options') purchase = self.find_element(*self._base_iframe_locator) self.driver.switch_to.frame(purchase) Utility.select(self.driver, self._state_locator, state) self.driver.switch_to.default_content()
def assignment_bar(self, name: str, _type: str = None): """Return the assignment bar for an assignment. :param str name: the assignment's name :param str _type: (optional) the assignment's type :return: the assignment status bar :rtype: :py:class:`~pages.tutor.task.AssignmentBar` """ sleep(0.5) assignments = [AssignmentBar(self, bar) for bar in self.find_elements(*self._assignment_bar_locator)] for assignment in assignments: if assignment.title == name: if _type and assignment.style != _type: continue return assignment raise TutorException(f'"{name}" not found in the currently ' + 'available assignments')