"""The dashboard (course picker) page object."""
from __future__ import annotations
from time import sleep
from pypom import Region
from selenium.common.exceptions import NoSuchElementException, TimeoutException
from selenium.webdriver.common.by import By
from pages.tutor.base import TutorBase
from utils.tutor import Tutor, TutorException
from utils.utilities import Utility, go_to_
[docs]class Dashboard(TutorBase):
"""The OpenStax Tutor Beta dashboard."""
URL_TEMPLATE = '/dashboard'
_root_locator = (By.CLASS_NAME, 'tutor-root')
_no_courses_locator = (By.CSS_SELECTOR, '.-course-list-empty')
_pending_verify_locator = (
By.CSS_SELECTOR, '.pending-faculty-verification')
_create_tile_locator = (By.CSS_SELECTOR, '.my-courses-add-zone')
_current_courses_locator = (By.CSS_SELECTOR, '.my-courses-current')
_preview_courses_locator = (By.CSS_SELECTOR, '.my-courses-preview')
_past_courses_locator = (By.CSS_SELECTOR, '.my-courses-past')
_tooltip_locator = (By.CSS_SELECTOR, '.joyride-tooltip')
@property
def nav(self):
"""Access the nav region.
:return: the general Tutor navigation
:rtype: :py:class:`~regions.tutor.nav.TutorNav`
"""
from regions.tutor.nav import TutorNav
return TutorNav(self)
@property
def no_courses(self):
"""Access the courseless section, if found.
:return: the courseless section or None
:rtype: :py:class:`~Dashboard.NoCourses`
"""
try:
root = self.find_element(*self._no_courses_locator)
return self.NoCourses(self, root)
except NoSuchElementException:
return None
@property
def pending(self):
"""Access the pending verification section, if found.
:return: the pending verification text region or None
:rtype: :py:class:`~Dashboard.Pending`
"""
try:
root = self.find_element(*self._pending_verify_locator)
return self.Pending(self, root)
except NoSuchElementException:
return None
@property
def current_courses(self):
"""Return the current courses section.
:return: the current courses region
:rtype: :py:class:`~pages.tutor.dashboard.Courses`
"""
return self._get_section(self._current_courses_locator)
[docs] 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))
@property
def preview_courses(self):
"""Return the preview courses section.
:return: the preview courses region
:rtype: :py:class:`~pages.tutor.dashboard.Courses`
"""
return self._get_section(self._preview_courses_locator)
@property
def past_courses(self):
"""Return the past courses section.
:return: the past courses region
:rtype: :py:class:`~pages.tutor.dashboard.Courses`
"""
return self._get_section(self._past_courses_locator)
def _get_section(self, locator):
"""Return the past courses section.
:param locator: a By-style Selenium selector for a course section
:type locator: (str, str)
:returns: a course selection region
:rtype: :py:class:`~pages.tutor.dashboard.Courses`
:noindex:
"""
try:
root = self.find_element(*locator)
except NoSuchElementException:
root = None
cycle = locator[1].split('-')[-1]
assert(root), "No {time} courses found".format(time=cycle)
return Courses(self, root)
[docs] def go_to_first_course(self):
"""Go to the first course.
:return: the calendar (teacher) or current week (student) page for the
first course in the list
:rtype: :py:class:`~pages.tutor.calendar.Calendar` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
course = self.current_courses.courses[0]
is_teacher = course.is_teacher
course.go_to_course()
if is_teacher:
from pages.tutor.calendar import Calendar
return go_to_(Calendar(self.driver, self.base_url))
from pages.tutor.course import StudentCourse
return go_to_(StudentCourse(self.driver, self.base_url))
[docs] 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}"')
@property
def tooltips_displayed(self):
"""Return True when the training wheels are visible.
:return: ``True`` if a training wheel modal is visible, else ``False``
:rtype: bool
"""
return bool(len(self.find_elements(*self._tooltip_locator)))
@property
def tooltips(self):
"""Access the tooltips, if found.
:return: a tooltip or None
:rtype: :py:class:`~regions.tutor.tooltip.Tooltip`
"""
tooltips = self.find_elements(*self._tooltip_locator)
if tooltips:
from regions.tutor.tooltip import Tooltip
return Tooltip(self, tooltips[0])
[docs] class NoCourses(Region):
"""No courses were found pane."""
_expanation_locator = (By.CSS_SELECTOR, '.lead')
_help_link_locator = (By.CSS_SELECTOR, 'a')
@property
def explanation(self):
"""Return the explanation text."""
return self.find_element(*self._expanation_locator).text
[docs] def get_help(self):
"""Click on the 'Get help >' link.
:return: the OpenStax knowledge base webpage in a new tab
:rtype: :py:class:`~pages.salesforce.home.Salesforce`
"""
link = self.find_element(*self._help_link_locator)
Utility.switch_to(self.driver, element=link)
from pages.salesforce.home import Salesforce
return go_to_(Salesforce(self.driver))
[docs] class Pending(Region):
"""The pending faculty verification pane."""
_title_locator = (By.CSS_SELECTOR, 'h4')
_expanation_locator = (By.CSS_SELECTOR, '.lead')
_chat_verify_locator = (By.CSS_SELECTOR, 'button')
@property
def title(self):
"""Return the pending title overlay text.
:return: the pending verification title overlay content
:rtype: str
"""
return self.find_element(*self._title_locator).text
@property
def explanation(self):
"""Return the explanation text.
:return: the pending verification explanation content
:rtype: str
"""
return self.find_element(*self._expanation_locator).text
@property
def verify_button(self):
r"""Return the verify now button.
:return: the verify button
:rtype: \
:py:class:`~selenium.webdriver.remote.webelement.WebElement`
"""
return self.find_element(*self._chat_verify_locator)
[docs] def verify_now(self):
"""Click the 'Verify now via chat' button.
:return: the Salesforce chat window
:rtype: :py:class:`~pages.salesforce.chat.Chat`
"""
Utility.switch_to(self.driver, element=self.verify_button)
from pages.salesforce.chat import Chat
return go_to_(Chat(self.driver))
[docs]class Courses(Region):
"""Courses sections."""
_course_locator = (By.CSS_SELECTOR, '.my-courses-item-wrapper')
@property
def courses(self):
"""Return a list of course objects.
:return: the list of available courses
:rtype: list(:py:class:`~pages.tutor.dashboard.Courses.Course`)
"""
return [self.Course(self, element)
for element in self.find_elements(*self._course_locator)]
[docs] 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}"')
[docs] def select_course_by_name(self, name, ignore_case=False, latest=True):
"""Select a course by the course name.
:param str name: the course name to select
:param bool ignore_case: (optional) match to the case-insensitive value
:param bool latest: (optional) use the most recent course in the option
list
:return: the calendar (teacher) or current week (student) page for the
selected course
:rtype: :py:class:`~pages.tutor.calendar.Calendar` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
return self._course_selection(
Tutor.BY_TITLE, name, ignore_case, latest)
[docs] def select_course_by_id(self, course_id):
"""Select a course by the course identification number.
:param str course_id: the course identification number to select
:return: the calendar (teacher) or current week (student) page for the
selected course
:rtype: :py:class:`~pages.tutor.calendar.Calendar` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
return self._course_selection(Tutor.BY_ID, course_id)
[docs] def select_course_by_subject(self, subject):
"""Select a random course by the book subject.
:param str subject: the course subject to select from
:return: the calendar (teacher) or current week (student) page for the
selected course
:rtype: :py:class:`~pages.tutor.calendar.Calendar` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
return self._course_selection(Tutor.BY_SUBJECT, subject)
[docs] def select_course_by_term(self, term, year=None):
"""Select a random course by the term and year.
:param str term: the semester or quarter to select from
:param str year: (optional) the 4-digit calendar year to select from
:return: the calendar (teacher) or current week (student) page for the
selected course
:rtype: :py:class:`~pages.tutor.calendar.Calendar` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
if not year:
from datetime import datetime
year = datetime.now().year
full_term = "{0} {1}".format(term, year)
return self._course_selection(Tutor.BY_TERM, full_term)
def _course_selection(self, option, value,
ignore_case=False, latest=False):
"""Select a specific or random course matching a particular option.
:param str option: the data option to retrieve
:param str value: the expected value to match a course against
:param bool ignore_case: (optional) match the value in a unicode-safe,
case-blind match
:param bool latest: (optional) select the newest course
:return: the calendar (teacher) or current week (student) page for the
selected course
:rtype: :py:class:`~pages.tutor.calendar.Calendar` or
:py:class:`~pages.tutor.course.StudentCourse`
:noindex:
"""
value = value.casefold() if ignore_case else value
course_options = []
for course in self.courses:
if value == course._get_data_option(option):
course_options.append(course)
assert(course_options), \
"No courses found for {0}:{1}".format(option, value)
course = 0 if latest else Utility.random(end=len(course_options))
return course_options[course].go_to_course()
[docs] class Course(Region):
"""Individual course cards."""
_course_info_locator = (By.CSS_SELECTOR, '[data-title]')
_card_locator = (By.CSS_SELECTOR, '.my-courses-item-title')
_card_safari_locator = (By.CSS_SELECTOR, '.my-courses-item-title a')
_preview_belt_locator = (By.CSS_SELECTOR, '.preview-belt p')
_course_brand_locator = (By.CSS_SELECTOR, '.course-branding')
_course_clone_locator = (By.CSS_SELECTOR, 'a')
@property
def course_info(self):
r"""Return the course data element.
:return: the course data object
:rtype: \
:py:class:`~selenium.webdriver.remote.webelement.WebElement`
"""
return self.find_element(*self._course_info_locator)
@property
def course_brand(self):
r"""Return the course brand element.
:return: the course branding object
:rtype: \
:py:class:`~selenium.webdriver.remote.webelement.WebElement`
"""
return self.find_element(*self._course_brand_locator)
@property
def course_clone(self):
r"""Return the course clone button.
:return: the course clone button
:rtype: \
:py:class:`~selenium.webdriver.remote.webelement.WebElement`
"""
return self.find_element(*self._course_clone_locator)
@property
def title(self):
"""Return the course title.
:return: the course title
:rtype: str
"""
return self.course_info.get_attribute("data-title")
@property
def book_title(self):
"""Return the textbook title.
:return: the course textbook title
:rtype: str
"""
return self.course_info.get_attribute("data-book-title")
@property
def appearance(self):
"""Return the book tile appearance code.
:return: the book tile appearance code
:rtype: str
"""
return self.course_info.get_attribute("data-appearance")
@property
def is_preview(self):
"""Return True if the course is a preview course.
:return: ``True`` if the course is a preview, else ``False``
:rtype: bool
"""
return (self.course_info
.get_attribute("data-is-preview").lower() == "true")
@property
def term(self):
"""Return the course term.
:return: the course semester or quarter and year
:rtype: str
"""
return self.course_info.get_attribute("data-term")
@property
def is_teacher(self):
"""Return True if the current user is a course instructor.
:return: ``True`` if the currently logged in user is an instructor
for the course, else ``False``
:rtype: bool
"""
return (self.course_info
.get_attribute("data-is-teacher").lower() == "true")
@property
def course_id(self):
"""Return the course identification number.
:return: the course identification number
:rtype: str
"""
return self.course_info.get_attribute("data-course-id")
@property
def course_type(self):
"""Return the course type.
Most courses will be Tutor, but some Concept
Coach courses may still show for old users.
:return: the course type
:rtype: str
"""
return (self.course_info
.get_attribute("data-course-course-type"))
[docs] def go_to_course(self):
"""Go to the course page for this course.
:return: the calendar (teacher) or current week (student) page for
the selected course
:rtype: :py:class:`~pages.tutor.calendar.Calendar` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
if self.is_teacher:
from pages.tutor.calendar import Calendar as Destination
else:
from pages.tutor.course import StudentCourse as Destination
if Utility.is_browser(self.driver, 'safari'):
card = self.find_element(*self._card_safari_locator)
else:
card = self.find_element(*self._card_locator)
Utility.click_option(self.driver, element=card)
return go_to_(Destination(self.driver, self.page.page.base_url))
[docs] def copy_this_course(self):
"""Clone the selected course.
:return: the course cloning wizard
:rtype: :py:class:`~pages.tutor.new_course.CloneCourse`
"""
assert(self.is_teacher), \
"Only verified instructors may clone a course"
assert(not self.is_preview), \
"Preview courses may not be cloned"
if Utility.is_browser(self.driver, 'safari'):
button = self.find_element(By.CSS_SELECTOR, '.btn-sm')
Utility.click_option(self.driver, element=button)
else:
from selenium.webdriver.common.action_chains import \
ActionChains
Utility.scroll_to(self.driver, element=self.root, shift=-80)
ActionChains(self.driver) \
.move_to_element(self.course_brand) \
.pause(1) \
.move_to_element(
self.find_element(By.CSS_SELECTOR, '.btn-sm')) \
.pause(1) \
.click() \
.perform()
from pages.tutor.new_course import CloneCourse
return go_to_(CloneCourse(self.driver, self.page.page.base_url))
@property
def preview_text(self):
"""Return the preview belt explanation text if found.
:return: the preview belt text if the course is a preview or an
empty string
:rtype: str
"""
if self.is_preview:
return self.find_element(*self._preview_belt_locator).text
return ""
def _get_data_option(self, option):
"""Return the property value by the option type.
:param str option: the option to select
:return: the value of the requested course data option
:rtype: str
:noindex:
"""
if option == Tutor.BY_TITLE:
return self.title
elif option == Tutor.BY_SUBJECT:
return self.book_title
elif option == Tutor.BY_APPEARANCE:
return self.appearance
elif option == Tutor.IS_PREVIEW:
return self.is_preview
elif option == Tutor.BY_TERM:
return self.term
elif option == Tutor.IS_TEACHER:
return self.is_teacher
elif option == Tutor.BY_ID:
return self.course_id
elif option == Tutor.BY_TYPE:
return self.course_type
raise ValueError('"{option}" is not a valid course data selector'
.format(option=option))