Source code for pages.tutor.new_course

"""The Tutor course creation wizard."""

from __future__ import annotations

from time import sleep
from typing import List, Union

from pypom import Region
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By

from pages.tutor.base import TutorBase
from pages.tutor.calendar import Calendar
from utils.tutor import TutorException
from utils.utilities import Utility, go_to_


[docs]class CloneCourse(TutorBase): """Clone an existing course.""" _loaded_pane_locator = (By.CSS_SELECTOR, '.new-course-wizard') _heading_locator = (By.CSS_SELECTOR, '.card-header') _cancel_button_locator = (By.CSS_SELECTOR, '.cancel') _back_button_locator = (By.CSS_SELECTOR, '.back') _continue_button_locator = (By.CSS_SELECTOR, '.next') _is_course_estimation_pane_locator = (By.CSS_SELECTOR, '.numbers') @property def loaded(self) -> bool: """Return True when the new course wizard root is found. :return: ``True`` when the new course wizard root element is located :rtype: bool """ return bool(self.find_elements(*self._loaded_pane_locator)) @property def heading(self) -> str: """Return the heading text. :return: the entire text heading :rtype: str """ return (self.find_element(*self._heading_locator) .get_attribute('textContent'))
[docs] def cancel(self) -> None: """Cancel the course creation wizard. :return: None """ button = self.find_element(*self._cancel_button_locator) Utility.click_option(self.driver, element=button)
[docs] def back(self) -> None: """Go to the previous step in the wizard. :return: None """ button = self.find_element(*self._back_button_locator) Utility.click_option(self.driver, element=button)
[docs] def next(self) -> Union[Calendar, None]: """Continue to the next step in the wizard. :return: the instructor's new calendar if on the course estimate step, otherwise None :rtype: :py:class:`~pages.tutor.calendar.Calendar` or NoneType """ go_to_calendar = self.is_course_estimate button = self.find_element(*self._continue_button_locator) Utility.click_option(self.driver, element=button) if go_to_calendar: self.wait.until(lambda _: 'new-course' not in self.location) sleep(0.5) return go_to_(Calendar(self.driver, base_url=self.base_url))
@property def is_course_estimate(self) -> bool: """Return True if on the course estimation step. :return: ``True`` if on the course estimation step, ``False`` if not :rtype: bool """ return bool( self.find_elements(*self._is_course_estimation_pane_locator)) @property def term(self) -> CloneCourse.Term: """Access the new course term pane. :return: the new course's term selection pane :rtype: :py:class:`~pages.tutor.new_course.CloneCourse.Term` """ return self.Term(self) @property def name(self) -> CloneCourse.Name: """Access the new course name pane. :return: the new course's name and timezone selection pane :rtype: :py:class:`~pages.tutor.new_course.CloneCourse.Name` """ return self.Name(self) @property def details(self) -> CloneCourse.Details: """Access the new course details pane. :return: the new course's section request and student estimate pane :rtype: :py:class:`~pages.tutor.new_course.CloneCourse.Details` """ return self.Details(self)
[docs] class Term(Region): """Select the course term.""" _course_term_locator = ( By.CSS_SELECTOR, '.choices-listing [role=button]') @property def loaded(self) -> bool: """Return True when course semesters/quarters are found. :return: ``True`` when course term options are found :rtype: bool :noindex: """ return bool(self.terms) @property def terms(self) -> List[CloneCourse.Term.Term]: r"""Access the available course terms. :return: the list of course terms available for new courses :rtype: list(:py:class:`~pages.tutor.new_course \ .CloneCourse.Term.Term`) """ return [self.Term(self, option) for option in self.find_elements(*self._course_term_locator)]
[docs] 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')
[docs] class Term(Region): """A course term.""" _term_locator = (By.CSS_SELECTOR, '.term') _year_locator = (By.CSS_SELECTOR, '.year') _option_locator = (By.CSS_SELECTOR, '.content') @property def term(self) -> str: """Return the semester or quarter. :return: the semester or quarter name :rtype: str """ return self.find_element(*self._term_locator).text @property def year(self) -> int: """Return the term year. :return: the term year :rtype: int """ return int(self.find_element(*self._year_locator).text) @property def selected(self) -> bool: """Return True if the term is currently selected. :return: ``True`` if the term is selected, otherwise ``False`` :rtype: bool """ return 'active' in self.root.get_attribute('class')
[docs] def select(self) -> None: """Click on the term option. :return: None """ button = self.find_element(*self._option_locator) Utility.click_option(self.driver, element=button)
[docs] class Name(Region): """Choose the course title or name.""" _course_name_input_locator = ( By.CSS_SELECTOR, '.course-details-name input') _select_course_timezone_locator = (By.CSS_SELECTOR, 'select') _timezone_options_locator = (By.CSS_SELECTOR, 'select option') @property def loaded(self) -> bool: """Return True when the course name input box is found. :return: ``True`` when the course name field is found :rtype: bool :noindex: """ return bool(self.find_elements(*self._course_name_input_locator)) @property def name(self) -> str: """Return the current value for the new course title. :return: the new course's name :rtype: str """ return (self.find_element(*self._course_name_input_locator) .get_attribute('value')) @name.setter def name(self, name: str) -> None: """Set the course title or name. .. note: The method uses the javascript value setter to overwrite whatever is in the name field. :param str name: the new course name :return: None """ name_box = self.find_element(*self._course_name_input_locator) Utility.clear_field(self.driver, field=name_box) sleep(0.25) name_box.send_keys(name) @property def timezone(self) -> str: """Return the currently selected timezone. :return: the current timezone string :rtype: str """ return (self.find_element(*self._select_course_timezone_locator) .get_attribute('value')) @timezone.setter def timezone(self, zone: str) -> None: """Set the course timezone. :param str zone: the new timezone for the course :return: None :raises :py:class:`~utils.tutor.TutorException`: if the new timezone does not match any of the available options """ options = [option.text for option in self.find_elements(*self._timezone_options_locator)] if zone not in options: raise TutorException( f'"{zone}" is not an available timezone option') Utility.select( driver=self.driver, element_locator=self._select_course_timezone_locator, label=zone)
[docs] class Details(Region): """Provide student information.""" _expanation_locator = (By.CSS_SELECTOR, 'form > div:not(.form-group)') _course_sections_locator = (By.CSS_SELECTOR, '#number-sections') _student_estimate_locator = (By.CSS_SELECTOR, '#number-students') _alert_error_message_locator = (By.CSS_SELECTOR, '[role=alert]') @property def loaded(self) -> bool: """Return True when the course details fields are found. :return: ``True`` when the course sections and estimated students fields are found. :rtype: bool :noindex: """ return (self.find_element(*self._course_sections_locator) and self.find_element(*self._student_estimate_locator) and (sleep(0.5) or True)) @property def explanation(self) -> str: """Return the course size estimation explanation. :return: the course size estimation explanation :rtype: str """ return (self.find_element(*self._expanation_locator) .get_attribute('textContent')) @property def sections(self) -> int: """Return the initial number of course sections or periods. :return: the requested number of course sections to automatically create or 0 if the value is not set :rtype: int """ try: return int(self.find_element(*self._course_sections_locator) .get_attribute('value')) except ValueError: return 0 @sections.setter 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()) @property def students(self) -> int: """Return the expected number of students. :return: the projected number of students who will enroll in the course or 0 if the value is not set :rtype: int """ try: return int(self.find_element(*self._student_estimate_locator) .get_attribute('value')) except ValueError: return 0 @students.setter 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()) @property def error(self) -> str: """Return the input error message, if present. .. note: If both cases are true (sections > 10 and students > 1500), then the first input issue is displayed. If the first issue is remedied, the second error replaces the first. :return: the error message when the number of sections is greater than 10 or the number of students is greater than 1,500 :rtype: str """ try: return (self.find_element(*self._alert_error_message_locator) .get_attribute('textContent')) except NoSuchElementException: return ''
[docs]class NewCourse(CloneCourse): """Create a new Tutor course.""" _new_or_copy_pane_locator = (By.CSS_SELECTOR, '.new_or_copy') @property def clone_possible(self) -> bool: """Return True if a previous course can be cloned. .. note: This method is expected to be run after selecting the course term. :return: ``True`` if a previous course exists and can be cloned, otherwise ``False`` :rtype: bool """ sleep(0.5) return bool(self.find_elements(*self._new_or_copy_pane_locator)) @property def course(self) -> NewCourse.Course: """Access the new course course pane. :return: the new course's section request and student estimate pane :rtype: :py:class:`~pages.tutor.new_course.NewCourse.Course` """ return self.Course(self) @property def new_or_clone(self) -> NewCourse.NewOrClone: """Access the new course details pane. :return: the new course's section request and student estimate pane :rtype: :py:class:`~pages.tutor.new_course.NewCourse.NewOrClone` """ return self.NewOrClone(self) @property def cloned_from(self) -> NewCourse.BaseCourse: """Access the new course details pane. :return: the new course's section request and student estimate pane :rtype: :py:class:`~pages.tutor.new_course.NewCourse.BaseCourse` """ return self.BaseCourse(self)
[docs] class Course(Region): """Select the course book.""" _book_option_locator = ( By.CSS_SELECTOR, '.choices-listing [role=button]') @property def loaded(self) -> bool: """Return True when the book options list is populated. :return: ``True`` when at least one book option is found :rtype: bool :noindex: """ return bool(self.books) @property def books(self) -> List[NewCourse.Course.Book]: r"""Access the course book options. :return: the list of available books :rtype: \ list(:py:class:`~pages.tutor.new_course.NewCourse.Course.Book`) """ return [self.Book(self, option) for option in self.find_elements(*self._book_option_locator)]
[docs] 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')
[docs] class Book(Region): """An available course book.""" _book_content_locator = (By.CSS_SELECTOR, '.content') @property def appearance(self) -> str: """Return the book appearance code. :return: the book appearance code :rtype: str """ return self.root.get_attribute('data-appearance') @property def is_selected(self) -> bool: """Return True if the book is currently selected. :return: ``True`` if the book is the current course selection, otherwise ``False`` :rtype: bool """ return 'active' in self.root.get_attribute('class') @property def title(self) -> str: """Return the book title. :return: the book title :rtype: str """ return self.find_element(*self._book_content_locator).text
[docs] def select(self) -> None: """Click on the book option. :return: None """ book = self.find_element(*self._book_content_locator) Utility.click_option(self.driver, element=book)
[docs] class NewOrClone(Region): """Create a new course or clone an existing course.""" _new_course_option_locator = ( By.CSS_SELECTOR, '[data-new-or-copy=new]') _clone_course_option_locator = ( By.CSS_SELECTOR, '[data-new-or-copy=copy]') @property def loaded(self) -> bool: """Return True when the new course option is found. :return: ``True`` when the 'Create a new course' option is found :rtype: bool :noindex: """ return bool(self.find_element(*self._new_course_option_locator))
[docs] def create_a_new_course(self) -> None: """Create a new course. Select the option to create a new course. :return: None """ new_course = self.find_element(*self._new_course_option_locator) Utility.click_option(self.driver, element=new_course)
[docs] def clone_a_past_course(self) -> None: """Clone an existing course. Select the option to clone an existing course. :return: None """ clone = self.find_element(*self._clone_course_option_locator) Utility.click_option(self.driver, element=clone)
[docs] class BaseCourse(Region): """Select the course to clone.""" _existing_course_option_locator = ( By.CSS_SELECTOR, '.choices-listing [role=button]') @property def loaded(self) -> bool: """Return True when a previous course option is found. :return: ``True`` when at least one course clone base is found :rtype: bool :noindex: """ return bool(self.courses) @property def courses(self) -> List[NewCourse.BaseCourse.Course]: r"""Access the previous course options. :return: the list of available courses to clone :rtype: list(:py:class:`~pages.tutor.new_course \ .NewCourse.BaseCourse.Course`) """ return [self.Course(self, option) for option in self.find_elements( *self._existing_course_option_locator)]
[docs] 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')
[docs] class Course(Region): """A previous course.""" _course_name_locator = (By.CSS_SELECTOR, '.title') _course_term_locator = (By.CSS_SELECTOR, '.sub-title') @property def name(self) -> str: """Return the existing course name. :return: the course name :rtype: str """ return self.find_element(*self._course_name_locator).text @property def term(self) -> str: """Return the existing course semester/quarter and year. :return: the course term :rtype: str """ return self.find_element(*self._course_term_locator).text
[docs] def select(self) -> None: """Click on the course option. :return: None """ Utility.click_option(self.driver, element=self.root)