@@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Studio/LMS: Added ability to set due date formatting through Studio's Advanced Settings.
|
||||
The key is due_date_display_format, and the value should be a format supported by Python's
|
||||
strftime function.
|
||||
|
||||
Common: Added configurable backends for tracking events. Tracking events using
|
||||
the python logging module is the default backend. Support for MongoDB and a
|
||||
Django database is also available.
|
||||
|
||||
@@ -67,7 +67,7 @@ def get_index(name):
|
||||
page_name_css = 'section[data-type="HTMLModule"]'
|
||||
all_pages = world.css_find(page_name_css)
|
||||
for i in range(len(all_pages)):
|
||||
if all_pages[i].html == '\n {name}\n'.format(name=name):
|
||||
if world.retry_on_exception(lambda: all_pages[i].html) == '\n {name}\n'.format(name=name):
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
import platform
|
||||
from urllib import quote_plus
|
||||
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@@ -45,18 +46,25 @@ def css_has_text(css_selector, text, index=0):
|
||||
|
||||
@world.absorb
|
||||
def wait_for(func, timeout=5):
|
||||
WebDriverWait(world.browser.driver, timeout).until(func)
|
||||
WebDriverWait(
|
||||
driver=world.browser.driver,
|
||||
timeout=timeout,
|
||||
ignored_exceptions=(StaleElementReferenceException)
|
||||
).until(func)
|
||||
|
||||
|
||||
def wait_for_present(css_selector, timeout=30):
|
||||
"""
|
||||
Waiting for the element to be present in the DOM.
|
||||
Throws an error if the wait_for time expires.
|
||||
Throws an error if the WebDriverWait timeout clock expires.
|
||||
Otherwise this method will return None
|
||||
"""
|
||||
try:
|
||||
WebDriverWait(driver=world.browser.driver,
|
||||
timeout=60).until(EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,)))
|
||||
WebDriverWait(
|
||||
driver=world.browser.driver,
|
||||
timeout=timeout,
|
||||
ignored_exceptions=(StaleElementReferenceException)
|
||||
).until(EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,)))
|
||||
except TimeoutException:
|
||||
raise TimeoutException("Timed out waiting for {} to be present.".format(css_selector))
|
||||
|
||||
@@ -65,12 +73,15 @@ def wait_for_present(css_selector, timeout=30):
|
||||
def wait_for_visible(css_selector, timeout=30):
|
||||
"""
|
||||
Waiting for the element to be visible in the DOM.
|
||||
Throws an error if the wait_for time expires.
|
||||
Throws an error if the WebDriverWait timeout clock expires.
|
||||
Otherwise this method will return None
|
||||
"""
|
||||
try:
|
||||
WebDriverWait(driver=world.browser.driver,
|
||||
timeout=timeout).until(EC.visibility_of_element_located((By.CSS_SELECTOR, css_selector,)))
|
||||
WebDriverWait(
|
||||
driver=world.browser.driver,
|
||||
timeout=timeout,
|
||||
ignored_exceptions=(StaleElementReferenceException)
|
||||
).until(EC.visibility_of_element_located((By.CSS_SELECTOR, css_selector,)))
|
||||
except TimeoutException:
|
||||
raise TimeoutException("Timed out waiting for {} to be visible.".format(css_selector))
|
||||
|
||||
@@ -79,12 +90,15 @@ def wait_for_visible(css_selector, timeout=30):
|
||||
def wait_for_invisible(css_selector, timeout=30):
|
||||
"""
|
||||
Waiting for the element to be either invisible or not present on the DOM.
|
||||
Throws an error if the wait_for time expires.
|
||||
Throws an error if the WebDriverWait timeout clock expires.
|
||||
Otherwise this method will return None
|
||||
"""
|
||||
try:
|
||||
WebDriverWait(driver=world.browser.driver,
|
||||
timeout=timeout).until(EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,)))
|
||||
WebDriverWait(
|
||||
driver=world.browser.driver,
|
||||
timeout=timeout,
|
||||
ignored_exceptions=(StaleElementReferenceException)
|
||||
).until(EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,)))
|
||||
except TimeoutException:
|
||||
raise TimeoutException("Timed out waiting for {} to be invisible.".format(css_selector))
|
||||
|
||||
@@ -93,14 +107,17 @@ def wait_for_invisible(css_selector, timeout=30):
|
||||
def wait_for_clickable(css_selector, timeout=30):
|
||||
"""
|
||||
Waiting for the element to be present and clickable.
|
||||
Throws an error if the wait_for time expires.
|
||||
Throws an error if the WebDriverWait timeout clock expires.
|
||||
Otherwise this method will return None.
|
||||
"""
|
||||
# Sometimes the element is clickable then gets obscured.
|
||||
# In this case, pause so that it is not reported clickable too early
|
||||
try:
|
||||
WebDriverWait(world.browser.driver,
|
||||
timeout=timeout).until(EC.element_to_be_clickable((By.CSS_SELECTOR, css_selector,)))
|
||||
WebDriverWait(
|
||||
driver=world.browser.driver,
|
||||
timeout=timeout,
|
||||
ignored_exceptions=(StaleElementReferenceException)
|
||||
).until(EC.element_to_be_clickable((By.CSS_SELECTOR, css_selector,)))
|
||||
except TimeoutException:
|
||||
raise TimeoutException("Timed out waiting for {} to be clickable.".format(css_selector))
|
||||
|
||||
@@ -133,7 +150,7 @@ def css_click(css_selector, index=0, wait_time=30):
|
||||
# another element might be on top of it. In this case, try
|
||||
# clicking in the upper left corner.
|
||||
try:
|
||||
return world.css_find(css_selector)[index].click()
|
||||
return retry_on_exception(lambda: world.css_find(css_selector)[index].click())
|
||||
|
||||
except WebDriverException:
|
||||
return css_click_at(css_selector, index=index)
|
||||
@@ -177,19 +194,19 @@ def id_click(elem_id):
|
||||
|
||||
@world.absorb
|
||||
def css_fill(css_selector, text, index=0):
|
||||
css_find(css_selector)[index].fill(text)
|
||||
retry_on_exception(lambda: css_find(css_selector)[index].fill(text))
|
||||
|
||||
|
||||
@world.absorb
|
||||
def click_link(partial_text, index=0):
|
||||
world.browser.find_link_by_partial_text(partial_text)[index].click()
|
||||
retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_text(css_selector, index=0, timeout=30):
|
||||
# Wait for the css selector to appear
|
||||
if is_css_present(css_selector):
|
||||
return css_find(css_selector, wait_time=timeout)[index].text
|
||||
return retry_on_exception(lambda: css_find(css_selector, wait_time=timeout)[index].text)
|
||||
else:
|
||||
return ""
|
||||
|
||||
@@ -198,7 +215,7 @@ def css_text(css_selector, index=0, timeout=30):
|
||||
def css_value(css_selector, index=0):
|
||||
# Wait for the css selector to appear
|
||||
if is_css_present(css_selector):
|
||||
return css_find(css_selector)[index].value
|
||||
return retry_on_exception(lambda: css_find(css_selector)[index].value)
|
||||
else:
|
||||
return ""
|
||||
|
||||
@@ -209,18 +226,18 @@ def css_html(css_selector, index=0):
|
||||
Returns the HTML of a css_selector
|
||||
"""
|
||||
assert is_css_present(css_selector)
|
||||
return css_find(css_selector)[index].html
|
||||
return retry_on_exception(lambda: css_find(css_selector)[index].html)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_has_class(css_selector, class_name, index=0):
|
||||
return css_find(css_selector)[index].has_class(class_name)
|
||||
return retry_on_exception(lambda: css_find(css_selector)[index].has_class(class_name))
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_visible(css_selector, index=0):
|
||||
assert is_css_present(css_selector)
|
||||
return css_find(css_selector)[index].visible
|
||||
return retry_on_exception(lambda: css_find(css_selector)[index].visible)
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -277,15 +294,21 @@ def trigger_event(css_selector, event='change', index=0):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def retry_on_exception(func, max_attempts=5):
|
||||
def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementReferenceException):
|
||||
"""
|
||||
Retry the interaction, ignoring the passed exceptions.
|
||||
By default ignore StaleElementReferenceException, which happens often in our application
|
||||
when the DOM is being manipulated by client side JS.
|
||||
Note that ignored_exceptions is passed directly to the except block, and as such can be
|
||||
either a single exception or multiple exceptions as a parenthesized tuple.
|
||||
"""
|
||||
attempt = 0
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
return func()
|
||||
break
|
||||
except WebDriverException:
|
||||
except ignored_exceptions:
|
||||
world.wait(1)
|
||||
attempt += 1
|
||||
except:
|
||||
attempt += 1
|
||||
|
||||
assert_true(attempt < max_attempts, 'Ran out of attempts to execute {}'.format(func))
|
||||
|
||||
@@ -227,17 +227,13 @@ class LTIModule(LTIFields, XModule):
|
||||
body=body,
|
||||
headers=headers)
|
||||
except ValueError: # scheme not in url
|
||||
# Stubbing headers for now:
|
||||
#https://github.com/idan/oauthlib/blob/master/oauthlib/oauth1/rfc5849/signature.py#L136
|
||||
#Stubbing headers for now:
|
||||
headers = {
|
||||
u'Content-Type': u'application/x-www-form-urlencoded',
|
||||
u'Authorization': u'oAuth ' # cont..
|
||||
u'oauth_nonce="80966668944732164491378916897", ' # cont..
|
||||
u'oauth_timestamp="1378916897", ' # cont..
|
||||
u'oauth_version="1.0", ' # cont..
|
||||
u'oauth_signature_method="HMAC-SHA1", ' # cont..
|
||||
u'oauth_consumer_key="", ' # cont..
|
||||
u'oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"',
|
||||
}
|
||||
u'Authorization': u'OAuth oauth_nonce="80966668944732164491378916897", \
|
||||
oauth_timestamp="1378916897", oauth_version="1.0", oauth_signature_method="HMAC-SHA1", \
|
||||
oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'}
|
||||
|
||||
params = headers['Authorization']
|
||||
# parse headers to pass to template as part of context:
|
||||
|
||||
@@ -1661,13 +1661,13 @@
|
||||
margin: 0 flex-gutter() 0 0;
|
||||
|
||||
.title {
|
||||
@extend .hd-lv4;
|
||||
@extend %hd-lv4;
|
||||
margin-bottom: ($baseline/4);
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .t-copy-sub1;
|
||||
@extend .t-weight3;
|
||||
@extend %t-copy-sub1;
|
||||
@extend %t-weight3;
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
@@ -1675,7 +1675,7 @@
|
||||
}
|
||||
|
||||
.action-verify label {
|
||||
@extend .t-copy-sub1;
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<div class="home">
|
||||
<a href="#" class="home-icon">
|
||||
<i class="icon icon-home"></i>
|
||||
<span class="text-sr">${_("Discussion Home")}</span>
|
||||
<span class="sr">${_("Discussion Home")}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="browse is-open">
|
||||
<a href="#" class="browse-topic-drop-icon">
|
||||
<i class="icon icon-reorder"></i>
|
||||
<span class="text-sr">${_("Discussion Topics")}</span>
|
||||
<span class="sr">${_("Discussion Topics")}</span>
|
||||
</a>
|
||||
<a href="#" class="browse-topic-drop-btn"><span class="current-board">${_("Show All Discussions")}</span> <span class="drop-arrow">▾</span></a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user