"""The instructor's course settings page."""
from time import sleep
from pypom import Region
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as expect
from pages.tutor.base import TutorBase
from utils.tutor import Tutor, TutorException
from utils.utilities import Utility, go_to_
# a javascript query to get the modal and tooltip root that is a neighbor of
# the React root element
GET_ROOT = 'return document.querySelector("[role={0}]");'
# -------------------------------------------------------- #
# Regions shared between the pages and the modals
# -------------------------------------------------------- #
[docs]class Component(Region):
"""A key or other data needed for LMS integration."""
_data_value_locator = (By.CSS_SELECTOR, 'input')
@property
def name(self):
"""Return the component's name.
:return: the component name
:rtype: str
"""
return self.root.text
@property
def value(self):
"""Return the key value or URL of the component.
:return: the value of the component
:rtype: str
"""
return (self.find_element(*self._data_value_locator)
.get_attribute('value'))
[docs]class Section(Region):
"""Section information."""
_enrollment_url_locator = (By.CSS_SELECTOR, 'input')
@property
def name(self):
"""Return the section name.
:return: the section or period name
:rtype: str
"""
return self.root.text
@property
def enrollment_url(self):
"""Return the enrollment URL for the section or period.
:return: the enrollment URL
:rtype: str
"""
return (self.find_element(*self._enrollment_url_locator)
.get_attribute('value'))
[docs]class Tab(Region):
"""A section tab."""
_name_locator = (By.CSS_SELECTOR, 'h2')
_select_tab_locator = (By.CSS_SELECTOR, 'a')
_settings_body_selector = '.student-access , .dates-and-times'
@property
def name(self):
"""Return the name listed on the tab.
:return: the name of the tab
:rtype: str
"""
return self.find_element(*self._name_locator).text
@property
def is_open(self):
"""Return True if the tab is currently selected.
:return: ``True`` if the tab is currently active, else ``False``
:rtype: bool
"""
return 'active' in self.root.get_attribute('class')
[docs] def select(self):
"""Click on the tab to open it.
:return: the course settings page
:rtype: :py:class:`CourseSettings`
"""
tab_name = self.name.lower()
tab = self.find_element(*self._select_tab_locator)
Utility.click_option(self.driver, element=tab)
sleep(0.25)
if 'student' in tab_name:
Destination = CourseSettings.StudentAccess
else:
Destination = CourseSettings.DatesAndTime
content_root = self.driver.execute_script(
'return document.querySelector("' +
f'{self._settings_body_selector}");')
return Destination(self.page, content_root)
[docs]class Vendor(Region):
"""An LMS vendor."""
_short_name_locator = (By.CSS_SELECTOR, 'input')
@property
def company(self):
"""Return the LMS vendor's company name.
:return: the LMS company name
:rtype: str
"""
return self.root.text
@property
def short_name(self):
"""Return the internal vendor short name.
:return: the LMS company short name
:rtype: str
"""
return (self.find_element(*self._short_name_locator)
.get_attribute('value'))
@property
def is_active(self):
"""Return True if the LMS vendor is currently selected.
:return: ``Ture`` if the vendor is currently selected else ``False``
:rtype: bool
"""
return 'active' in self.root.get_attribute('class')
# -------------------------------------------------------- #
# Course settings modals
# -------------------------------------------------------- #
[docs]class CourseSettingsModal(Region):
"""The base model for course settings modals."""
_title_locator = (By.CSS_SELECTOR, '.modal-title')
_close_x_locator = (By.CSS_SELECTOR, '.close')
_close_locator = (By.CSS_SELECTOR, '.modal-footer button:last-child')
@property
def title(self):
"""Return the modal title."""
return self.find_element(*self._title_locator).text
[docs] def close(self, x_button=False):
"""Click the 'Close', 'Cancel', 'Rename', 'Save', or 'x' button.
:param bool x_button: if ``True`` use the 'x' button in the upper right
of the modal, otherwise use the footer button
:return: the course settings page
:rtype: :py:class:`CourseSettings`
"""
locator = self._close_x_locator if x_button else self._close_locator
button = self.find_element(*locator)
Utility.click_option(self.driver, element=button)
sleep(0.25)
self.wait.until(expect.staleness_of(self.root))
return self.page
[docs]class ChangeCourseTimezone(CourseSettingsModal):
"""The timezone change modal."""
_timezone_radio_locator = (By.CSS_SELECTOR, '.tutor-radio')
_active_timezone_locator = (By.CSS_SELECTOR, '.tutor-radio.active')
_time_preview_locator = (By.CSS_SELECTOR, '.timezone-preview')
@property
def timezones(self):
"""Access the available timezones for the course.
:return: the list of available timezones
:rtype: list(:py:class:`~ChangeCourseTimezone.Timezone`)
"""
return [self.Timezone(self, zone)
for zone in self.find_elements(*self._timezone_radio_locator)]
@property
def active(self):
"""Return the currently selected timezone.
:return: the current timezone in use by the course
:rtype: :py:class:`~ChangeCourseTimezone.Timezone`
"""
current_timezone = self.find_element(*self._active_timezone_locator)
return self.Timezone(self, current_timezone)
[docs] 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))
@property
def preview(self):
"""Return the preview time if using the selected timezone.
:return: the preview time format if the selected timezone is accepted
:rtype: str
"""
return self.find_element(*self._time_preview_locator).text
[docs] def save(self):
"""Click on the Save button.
:return: the course settings page
:rtype: :py:class:`CourseSettings`
"""
return self.close(x_button=False)
[docs] class Timezone(Region):
"""A timezone option for a course."""
_name_locator = (By.CSS_SELECTOR, 'label')
_button_locator = (By.CSS_SELECTOR, 'input')
@property
def name(self):
"""Return the timezone name or region name.
:return: the name or affected region for the timezone setting
:rtype: str
"""
return self.find_element(*self._name_locator).text
[docs] def select(self):
"""Click on the timezone radio button.
:return: the timezone modal
:rtype: :py:class:`ChangeCourseTimezone`
"""
button = self.find_element(*self._button_locator)
Utility.click_option(self.driver, element=button)
sleep(0.25) # to allow the preview time to update
return self.page
[docs] def is_current(self):
"""Return True if the timezone is currently selected.
:return: ``True`` if the timezone option is currently selected,
else ``False``
:rtype: bool
"""
return 'active' in self.root.get_attribute('class')
[docs]class DirectLinks(CourseSettingsModal):
"""The use 'Direct links instead of LMS integration' modal."""
_content_locator = (By.CSS_SELECTOR, '.modal-body')
_im_sure_button_locator = (
By.CSS_SELECTOR, '.modal-footer button:first-child')
@property
def description(self):
"""Return the modal body content.
:return: the text explaining what will happen if LMS integration is
deactivated
:rtype: str
"""
return self.find_element(*self._content_locator).text
[docs] def im_sure(self):
"""Click on the "I'm sure" button.
Clicking on the button switches the enrollment option back to 'Give
students direct links' and closes the modal.
:return: the course settings page
:rtype: :py:class:`CourseSettings`
"""
button = self.find_element(*self._im_sure_button_locator)
Utility.click_option(self.driver, element=button)
sleep(0.25)
return self.page
[docs] def cancel(self):
"""Click on the 'Cancel' button.
Close the modal without changing the enrollment option.
:return: the course settings page
:rtype: :py:class:`CourseSettings`
"""
return self.close(x_button=False)
[docs]class LMSKeyPairings(CourseSettingsModal):
"""Key pairings review modal."""
_lms_option_locator = (By.CSS_SELECTOR, '.lms-access label.button')
_instructions_locator = (By.CSS_SELECTOR, '.lms-access p')
_integration_help_locator = (By.CSS_SELECTOR, '.external-icon')
_control_locator = (By.CSS_SELECTOR, 'label.copy-on-focus')
@property
def lms_option(self):
"""Access the LMS vendor options.
:return: the LMS vendor options
:rtype: list(:py:class:`Vendor`)
"""
return [Vendor(self, option)
for option in self.find_elements(*self._lms_option_locator)]
@property
def instructions(self):
"""Return the integration instructions.
:return: the basic LMS integration instructions
:rtype: str
"""
return self.find_element(*self._instructions_locator).text
[docs] def how_do_i_integrate_with(self):
"""Access the Salesforce LMS integration walkthrough.
:return: the LMS integration walkthrough on Salesforce
:rtype: :py:class:`~pages.salesforce.home.Salesforce`
"""
link = self.find_element(*self._integration_help_locator)
Utility.switch_to(self.driver, element=link)
from pages.salesforce.home import Salesforce
return go_to_(Salesforce(self.driver))
@property
def controls(self):
"""Access the keys and values to use LMS integration.
:return: the keys, secrets, URL controls necessary to integrate with
the selected LMS system
:rtype: list(:py:class:`Component`)
"""
return [Component(self, value)
for value in self.find_elements(*self._control_locator)]
[docs]class RenameCourse(CourseSettingsModal):
"""The course renaming modal."""
_course_name_input_locator = (By.CSS_SELECTOR, 'input')
@property
def name(self):
"""Return the course name.
:return: the current value of the course name input text box
:rtype: str
"""
return (self.find_element(*self._course_name_input_locator)
.get_attribute('value'))
@name.setter
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
[docs] def rename(self):
"""Click on the 'Rename' button.
:return: the course settings page with the new course title displayed
:rtype: :py:class:`CourseSettings`
"""
return self.close(x_button=False)
# -------------------------------------------------------- #
# The Course Settings page
# -------------------------------------------------------- #
[docs]class CourseSettings(TutorBase):
"""The Tutor course settings page."""
_page_title_locator = (By.CSS_SELECTOR, '.title')
_course_name_locator = (By.CSS_SELECTOR, '.course-settings-title')
_edit_course_name_button_locator = (
By.CSS_SELECTOR, '.course-settings-title button')
_tab_locator = (By.CSS_SELECTOR, '[role=tab]')
_active_page_tab_locator = (By.CSS_SELECTOR, '.nav-tabs li.active h2')
_course_term_locator = (By.CSS_SELECTOR, '.course-settings-term')
_settings_body_locator = (
By.CSS_SELECTOR, Tab._settings_body_selector)
@property
def loaded(self) -> bool:
"""Return True when the settings page active nav tab is found.
:return: ``True`` when the internal active nav tab is found
:rtype: bool
"""
return bool(self.find_elements(*self._active_page_tab_locator))
@property
def page_title(self):
"""Return the page title.
:return: the page title
:rtype: str
"""
return self.find_element(*self._page_title_locator).text
@property
def course_name(self):
"""Return the course name.
:return: the course name
:rtype: str
"""
return self.find_element(*self._course_name_locator).text
[docs] def edit_course_name(self):
"""Click on the edit icon next to the course name.
:return: the course rename modal
:rtype: :py:class:`RenameCourse`
"""
button = self.find_element(*self._edit_course_name_button_locator)
Utility.click_option(self.driver, element=button)
sleep(0.25)
modal_root = self.driver.execute_script(GET_ROOT.format('dialog'))
return RenameCourse(self, modal_root)
@property
def tabs(self):
"""Access the page tabs.
:return: the student access and dates and time tabs
:rtype: list(:py:class:`Tab`)
"""
return [Tab(self, option)
for option in self.find_elements(*self._tab_locator)]
[docs] def student_access(self) -> None:
"""Click on the 'STUDENT ACCESS' tab.
:return: None
"""
return (sleep(0.5) or self.tabs[Tutor.STUDENT_ACCESS].select())
[docs] def dates_and_time(self) -> None:
"""Click on the 'DATES AND TIME' tab.
:return: None
"""
return (sleep(0.5) or self.tabs[Tutor.DATES_AND_TIME].select())
@property
def term(self):
"""Return the course term.
:return: the course term
:rtype: str
"""
return self.find_element(*self._course_term_locator).text
@property
def content(self):
"""Access the main page content.
:return: the student access content or the dates and times content
:rtype: :py:class:`~CourseSettings.StudentAccess` or
:py:class:`~CourseSettings.DatesAndTime`
"""
active = (self.find_element(*self._active_page_tab_locator)
.get_attribute('textContent')
.lower())
body_root = self.find_element(*self._settings_body_locator)
if 'access' in active:
return self.StudentAccess(self, body_root).panel
return self.DatesAndTime(self, body_root)
[docs] class StudentAccess(Region):
"""The course settings student access panel split.
This is a pass-through region to select the correct setup for the
course.
"""
_lms_disabled_locator = (By.CSS_SELECTOR, '.direct-links-only')
_lms_enabled_with_enrolled_locator = (By.CSS_SELECTOR, '.enrolled')
_lms_enabled_without_enrolled_locator = (By.CSS_SELECTOR, '.card-body')
@property
def panel(self):
"""Return the correct subclass panel.
:return: the subpanel according to the course settings
:rtype: :py:class:`~CourseSettings.StudentAccess.DirectAccess` or
:py:class:`~CourseSettings.StudentAccess.DirectLinksOnly` or
:py:class:`~CourseSettings.StudentAccess.Enrolled` or
:py:class:`~CourseSettings.StudentAccess.LMS`
"""
try:
self.find_element(*self._lms_disabled_locator)
return self.DirectLinksOnly(self, self.root)
except NoSuchElementException:
try:
self.find_element(*self._lms_enabled_with_enrolled_locator)
return self.Enrolled(self, self.root)
except NoSuchElementException:
enabled = self.find_elements(
*self._lms_enabled_without_enrolled_locator)
if Utility.has_children(enabled[1]):
return self.LMS(self, self.root)
else:
return self.DirectAccess(self, self.root)
[docs] class DirectAccess(Region):
"""Direct student enrollment links."""
_title_locator = (By.CSS_SELECTOR, '.title')
_information_locator = (By.CSS_SELECTOR, '.info')
_section_locator = (By.CSS_SELECTOR, '.card-body > label')
@property
def title(self):
"""Return the section title.
:return: the panel section title
:rtype: str
"""
return self.find_element(*self._title_locator).text
@property
def information(self):
"""Return the enrollment information help text.
:return: the direct link explanation text
:rtype: str
"""
return self.find_element(*self._information_locator).text
@property
def sections(self):
"""Access the individual section URLs.
:return: the list of available section or period enrollment
URLs
:rtype: list(:py:class:`Section`)
"""
return [Section(self, section)
for section
in self.find_elements(*self._section_locator)]
[docs] class DirectLinksOnly(Region):
"""The LMS option is not available for the course."""
_integration_help_locator = (By.CSS_SELECTOR, '.external-icon')
_section_locator = (By.CSS_SELECTOR, 'label')
[docs] def find_out_more(self):
"""Access the Salesforce LMS integration overview.
:return: the LMS integration walkthrough on Salesforce
:rtype: :py:class:`~pages.salesforce.home.Salesforce`
"""
link = self.find_element(*self._integration_help_locator)
Utility.switch_to(self.driver, element=link)
from pages.salesforce.home import Salesforce
return go_to_(Salesforce(self.driver))
@property
def sections(self):
"""Access the individual section URLs.
:return: the list of available section or period enrollment
URLs
:rtype: list(:py:class:`Section`)
"""
return [Section(self, section)
for section
in self.find_elements(*self._section_locator)]
[docs] class Enrolled(Region):
"""The LMS option is enabled and students have registered."""
_explanation_locator = (By.CSS_SELECTOR, 'p')
_show_lms_keys_locator = (By.CSS_SELECTOR, 'a')
@property
def explanation(self):
"""Return the enrollment explanation for LMS students.
:return: the enrollment explanation for LMS students
:rtype: str
"""
return self.find_element(*self._explanation_locator).text
[docs] def show_lms_keys_again(self):
"""Click the link and display the LMS key modal.
:return: the LMS key modal
:rtype: :py:class:`LMSKeyPairings`
"""
link = self.find_element(*self._show_lms_keys_locator)
Utility.click_option(self.driver, element=link)
sleep(0.25)
modal_root = self.driver.execute_script(
GET_ROOT.format('dialog'))
return LMSKeyPairings(self.page.page, modal_root)
[docs] class LMS(Region):
"""LMS access."""
_title_locator = (By.CSS_SELECTOR, '.title')
_information_locator = (By.CSS_SELECTOR, '.info')
_vendor_locator = (By.CSS_SELECTOR, '.lms-access label')
_integration_help_locator = (
By.CSS_SELECTOR, '.lms-access .external-icon')
_control_locator = (By.CSS_SELECTOR, '.lms-access .copy-on-focus')
@property
def title(self):
"""Return the section title.
:return: the panel section title
:rtype: str
"""
return self.find_element(*self._title_locator).text
@property
def information(self):
"""Return the enrollment information help text.
:return: the direct link explanation text
:rtype: str
"""
return self.find_element(*self._information_locator).text
@property
def vendors(self):
"""Access the list of LMS vendors.
:return: the list of available LMS providers
:rtype: list(:py:class:`Vendor`)
"""
return [Vendor(self, option)
for option
in self.find_elements(*self._vendor_locator)]
[docs] def how_do_i_integrate_with(self):
"""Access the Salesforce LMS integration walkthrough.
:return: the LMS integration walkthrough on Salesforce
:rtype: :py:class:`~pages.salesforce.home.Salesforce`
"""
link = self.find_element(*self._integration_help_locator)
Utility.switch_to(self.driver, element=link)
from pages.salesforce.home import Salesforce
return go_to_(Salesforce(self.driver))
@property
def controls(self):
"""Access the keys and values to use LMS integration.
:return: the keys, secrets, URL controls necessary to integrate
with the selected LMS system
:rtype: list(:py:class:`Component`)
"""
return [Component(self, value)
for value
in self.find_elements(*self._control_locator)]
[docs] class DatesAndTime(Region):
"""The course settings dates and time panel."""
_course_term_locator = (
By.CSS_SELECTOR, '.dates-and-times div:first-child')
_start_and_end_dates_locator = (
By.CSS_SELECTOR, '.dates-and-times div:nth-child(2)')
_timezone_locator = (
By.CSS_SELECTOR, '.dates-and-times div:last-child')
_edit_timezone_button_locator = (
By.CSS_SELECTOR, '.dates-and-times button')
@property
def term(self):
"""Return the course term.
:return: the course term
:rtype: str
"""
return self.find_element(*self._course_term_locator).text
@property
def course_dates(self):
"""Return the start and end date text.
:return: the start and end date text string
:rtype: str
"""
return self.find_element(*self._start_and_end_dates_locator).text
@property
def start(self):
"""Return the course start date.
:return: the course starting date string
:rtype: str
"""
return self.course_dates.split()[Tutor.START_DATE]
@property
def end(self):
"""Return the course end date.
:return: the course ending date string
:rtype: str
"""
return self.course_dates.split()[Tutor.END_DATE]
@property
def timezone(self):
"""Return the assigned course timezone.
:return: the course timezone
:rtype: str
"""
return self.find_element(*self._timezone_locator).text.strip()
[docs] def edit_timezone(self):
"""Click on the edit icon nex to the course timezone.
:return: the change course timezone modal
:rtype: :py:class:`ChangeCourseTimezone`
"""
button = self.find_element(*self._edit_timezone_button_locator)
Utility.click_option(self.driver, element=button)
sleep(0.25)
modal_root = self.driver.execute_script(GET_ROOT.format('dialog'))
return ChangeCourseTimezone(self.page, modal_root)