Merge remote-tracking branch 'origin/master' into will/combine-reg-login-form

Conflicts:
	lms/djangoapps/student_account/test/test_views.py
	lms/djangoapps/student_account/urls.py
	lms/djangoapps/student_account/views.py
This commit is contained in:
Will Daly
2014-10-20 10:57:05 -04:00
591 changed files with 52480 additions and 14599 deletions

View File

@@ -61,7 +61,7 @@ def select_contribution(amount=32):
def click_verified_track_button():
world.wait_for_ajax_complete()
btn_css = 'input[value="Select Certificate"]'
btn_css = 'input[value="Pursue a Verified Certificate"]'
world.css_click(btn_css)

View File

@@ -6,7 +6,8 @@ from django.conf import settings
from mock import patch
from pytz import UTC
from splinter.exceptions import ElementDoesNotExist
from nose.tools import assert_true, assert_equal, assert_in
from selenium.common.exceptions import NoAlertPresentException
from nose.tools import assert_true, assert_equal, assert_in, assert_is_none
from lettuce import world, step
from courseware.tests.factories import InstructorFactory, BetaTesterFactory
@@ -72,13 +73,39 @@ def view_lti_permission_alert(_step):
assert len(world.browser.windows) == 1
def check_no_alert():
"""
Make sure the alert has gone away.
Note that the splinter documentation indicates that
get_alert should return None if no alert is present,
however that is not the case. Instead a
NoAlertPresentException is raised.
"""
try:
assert_is_none(world.browser.get_alert())
except NoAlertPresentException:
pass
@step('I accept the permission alert and view the LTI$')
def accept_lti_permission_alert(_step):
parent_window = world.browser.current_window # Save the parent window
# To start with you should only have one window/tab
assert len(world.browser.windows) == 1
alert = world.browser.get_alert()
alert.accept()
assert len(world.browser.windows) != 1
check_no_alert()
# Give it a few seconds for the LTI window to appear
world.wait_for(
lambda _: len(world.browser.windows) == 2,
timeout=5,
timeout_msg="Timed out waiting for the LTI window to appear."
)
# Verify the LTI window
check_lti_popup(parent_window)
@@ -86,6 +113,7 @@ def accept_lti_permission_alert(_step):
def reject_lti_permission_alert(_step):
alert = world.browser.get_alert()
alert.dismiss()
check_no_alert()
assert len(world.browser.windows) == 1
@@ -234,20 +262,29 @@ def i_am_registered_for_the_course(coursenum, metadata, user='Instructor'):
def check_lti_popup(parent_window):
assert len(world.browser.windows) != 1
# You should now have 2 browser windows open, the original courseware and the LTI
windows = world.browser.windows
assert_equal(len(windows), 2)
for window in world.browser.windows:
world.browser.switch_to_window(window) # Switch to a different window (the pop-up)
# Check if this is the one we want by comparing the url
url = world.browser.url
basename = os.path.basename(url)
pathname = os.path.splitext(basename)[0]
if pathname == u'correct_lti_endpoint':
break
# For verification, iterate through the window titles and make sure that
# both are there.
tabs = []
for window in windows:
world.browser.switch_to_window(window)
tabs.append(world.browser.title)
assert_equal(tabs, [u'LTI | Test Section | test_course Courseware | edX', u'TEST TITLE'])
# Now verify the contents of the LTI window (which is the 2nd window/tab)
# Note: The LTI opens in a new browser window, but Selenium sticks with the
# current window until you explicitly switch to the context of the new one.
world.browser.switch_to_window(windows[1])
url = world.browser.url
basename = os.path.basename(url)
pathname = os.path.splitext(basename)[0]
assert_equal(pathname, u'correct_lti_endpoint')
result = world.css_find('.result').first.text
assert result == u'This is LTI tool. Success.'
assert_equal(result, u'This is LTI tool. Success.')
world.browser.driver.close() # Close the pop-up window
world.browser.switch_to_window(parent_window) # Switch to the main window again

View File

@@ -17,7 +17,7 @@ class I18nTestCase(TestCase):
self.assertIn('<html lang="en">', response.content)
self.assertEqual(response['Content-Language'], 'en')
self.assertTrue(re.search('<body.*class=".*lang_en">', response.content))
def test_esperanto(self):
response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='eo')
self.assertIn('<html lang="eo">', response.content)

View File

@@ -441,8 +441,10 @@ class TestGetHtmlMethod(BaseTestXmodule):
{sources}
</video>
"""
data = {
'download_video': 'true',
# test with download_video set to false and make sure download_video_link is not set (is None)
'download_video': 'false',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
@@ -450,12 +452,11 @@ class TestGetHtmlMethod(BaseTestXmodule):
""",
'edx_video_id': "mock item",
'result': {
'download_video_link': u'http://www.meowmix.com',
'download_video_link': None,
'sources': json.dumps([u'example.mp4', u'example.webm']),
}
}
# Video found for edx_video_id
initial_context = {
'data_dir': getattr(self, 'data_dir', None),

View File

@@ -11,7 +11,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from django.core.urlresolvers import reverse
from util.testing import UrlResetMixin
from django_comment_client.tests.group_id import (
GroupIdAssertionMixin,
CohortedTopicGroupIdTestMixin,
NonCohortedTopicGroupIdTestMixin
)
@@ -570,7 +569,13 @@ class ForumFormDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopicG
class UserProfileDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdTestMixin):
cs_endpoint = "/active_threads"
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True, is_ajax=False):
def call_view_for_profiled_user(
self, mock_request, requesting_user, profiled_user, group_id, pass_group_id, is_ajax=False
):
"""
Calls "user_profile" view method on behalf of "requesting_user" to get information about
the user "profiled_user".
"""
kwargs = {}
if group_id:
kwargs['group_id'] = group_id
@@ -587,12 +592,17 @@ class UserProfileDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopi
data=request_data,
**headers
)
request.user = user
request.user = requesting_user
mako_middleware_process_request(request)
return views.user_profile(
request,
self.course.id.to_deprecated_string(),
user.id
profiled_user.id
)
def call_view(self, mock_request, _commentable_id, user, group_id, pass_group_id=True, is_ajax=False):
return self.call_view_for_profiled_user(
mock_request, user, user, group_id, pass_group_id=pass_group_id, is_ajax=is_ajax
)
def test_group_info_in_html_response(self, mock_request):
@@ -617,6 +627,109 @@ class UserProfileDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopi
response, lambda d: d['discussion_data'][0]
)
def _test_group_id_passed_to_user_profile(
self, mock_request, expect_group_id_in_request, requesting_user, profiled_user, group_id, pass_group_id
):
"""
Helper method for testing whether or not group_id was passed to the user_profile request.
"""
def get_params_from_user_info_call(for_specific_course):
"""
Returns the request parameters for the user info call with either course_id specified or not,
depending on value of 'for_specific_course'.
"""
# There will be 3 calls from user_profile. One has the cs_endpoint "active_threads", and it is already
# tested. The other 2 calls are for user info; one of those calls is for general information about the user,
# and it does not specify a course_id. The other call does specify a course_id, and if the caller did not
# have discussion moderator privileges, it should also contain a group_id.
for r_call in mock_request.call_args_list:
if not r_call[0][1].endswith(self.cs_endpoint):
params = r_call[1]["params"]
has_course_id = "course_id" in params
if (for_specific_course and has_course_id) or (not for_specific_course and not has_course_id):
return params
self.assertTrue(
False,
"Did not find appropriate user_profile call for 'for_specific_course'=" + for_specific_course
)
mock_request.reset_mock()
self.call_view_for_profiled_user(
mock_request,
requesting_user,
profiled_user,
group_id,
pass_group_id=pass_group_id,
is_ajax=False
)
# Should never have a group_id if course_id was not included in the request.
params_without_course_id = get_params_from_user_info_call(False)
self.assertNotIn("group_id", params_without_course_id)
params_with_course_id = get_params_from_user_info_call(True)
if expect_group_id_in_request:
self.assertIn("group_id", params_with_course_id)
self.assertEqual(group_id, params_with_course_id["group_id"])
else:
self.assertNotIn("group_id", params_with_course_id)
def test_group_id_passed_to_user_profile_student(self, mock_request):
"""
Test that the group id is always included when requesting user profile information for a particular
course if the requester does not have discussion moderation privileges.
"""
def verify_group_id_always_present(profiled_user, pass_group_id):
"""
Helper method to verify that group_id is always present for student in course
(non-privileged user).
"""
self._test_group_id_passed_to_user_profile(
mock_request, True, self.student, profiled_user, self.student_cohort.id, pass_group_id
)
# In all these test cases, the requesting_user is the student (non-privileged user).
# The profile returned on behalf of the student is for the profiled_user.
verify_group_id_always_present(profiled_user=self.student, pass_group_id=True)
verify_group_id_always_present(profiled_user=self.student, pass_group_id=False)
verify_group_id_always_present(profiled_user=self.moderator, pass_group_id=True)
verify_group_id_always_present(profiled_user=self.moderator, pass_group_id=False)
def test_group_id_user_profile_moderator(self, mock_request):
"""
Test that the group id is only included when a privileged user requests user profile information for a
particular course and user if the group_id is explicitly passed in.
"""
def verify_group_id_present(profiled_user, pass_group_id, requested_cohort=self.moderator_cohort):
"""
Helper method to verify that group_id is present.
"""
self._test_group_id_passed_to_user_profile(
mock_request, True, self.moderator, profiled_user, requested_cohort.id, pass_group_id
)
def verify_group_id_not_present(profiled_user, pass_group_id, requested_cohort=self.moderator_cohort):
"""
Helper method to verify that group_id is not present.
"""
self._test_group_id_passed_to_user_profile(
mock_request, False, self.moderator, profiled_user, requested_cohort.id, pass_group_id
)
# In all these test cases, the requesting_user is the moderator (privileged user).
# If the group_id is explicitly passed, it will be present in the request.
verify_group_id_present(profiled_user=self.student, pass_group_id=True)
verify_group_id_present(profiled_user=self.moderator, pass_group_id=True)
verify_group_id_present(
profiled_user=self.student, pass_group_id=True, requested_cohort=self.student_cohort
)
# If the group_id is not explicitly passed, it will not be present because the requesting_user
# has discussion moderator privileges.
verify_group_id_not_present(profiled_user=self.student, pass_group_id=False)
verify_group_id_not_present(profiled_user=self.moderator, pass_group_id=False)
@patch('lms.lib.comment_client.utils.requests.request')
class FollowedThreadsDiscussionGroupIdTestCase(CohortedContentTestCase, CohortedTopicGroupIdTestMixin):

View File

@@ -40,7 +40,7 @@ def _attr_safe_json(obj):
return saxutils.escape(json.dumps(obj), {'"': '&quot;'})
@newrelic.agent.function_trace()
def make_course_settings(course, include_category_map=False):
def make_course_settings(course):
"""
Generate a JSON-serializable model for course settings, which will be used to initialize a
DiscussionCourseSettings object on the client.
@@ -51,11 +51,9 @@ def make_course_settings(course, include_category_map=False):
'allow_anonymous': course.allow_anonymous,
'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
'cohorts': [{"id": str(g.id), "name": g.name} for g in get_course_cohorts(course)],
'category_map': utils.get_discussion_category_map(course)
}
if include_category_map:
obj['category_map'] = utils.get_discussion_category_map(course)
return obj
@newrelic.agent.function_trace()
@@ -167,7 +165,7 @@ def forum_form_discussion(request, course_id):
nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key, check_if_enrolled=True)
course_settings = make_course_settings(course, include_category_map=True)
course_settings = make_course_settings(course)
user = cc.User.from_django_user(request.user)
user_info = user.to_dict()
@@ -231,7 +229,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load_forum', course_key)
course_settings = make_course_settings(course, include_category_map=True)
course_settings = make_course_settings(course)
cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict()
is_moderator = cached_has_permission(request.user, "see_all_cohorts", course_key)
@@ -325,8 +323,6 @@ def user_profile(request, course_id, user_id):
#TODO: Allow sorting?
course = get_course_with_access(request.user, 'load_forum', course_key)
try:
profiled_user = cc.User(id=user_id, course_id=course_key)
query_params = {
'page': request.GET.get('page', 1),
'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities
@@ -338,6 +334,9 @@ def user_profile(request, course_id, user_id):
return HttpResponseBadRequest("Invalid group_id")
if group_id is not None:
query_params['group_id'] = group_id
profiled_user = cc.User(id=user_id, course_id=course_key, group_id=group_id)
else:
profiled_user = cc.User(id=user_id, course_id=course_key)
threads, page, num_pages = profiled_user.active_threads(query_params)
query_params['page'] = page

View File

@@ -30,8 +30,8 @@ from instructor_task.tasks_helper import (
rescore_problem_module_state,
reset_attempts_module_state,
delete_problem_module_state,
push_grades_to_s3,
push_students_csv_to_s3
upload_grades_csv,
upload_students_csv
)
from bulk_email.tasks import perform_delegate_email_batches
@@ -139,7 +139,7 @@ def calculate_grades_csv(entry_id, xmodule_instance_args):
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name = ugettext_noop('graded')
task_fn = partial(push_grades_to_s3, xmodule_instance_args)
task_fn = partial(upload_grades_csv, xmodule_instance_args)
return run_main_task(entry_id, task_fn, action_name)
@@ -151,5 +151,5 @@ def calculate_students_features_csv(entry_id, xmodule_instance_args):
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name = ugettext_noop('generated')
task_fn = partial(push_students_csv_to_s3, xmodule_instance_args)
task_fn = partial(upload_students_csv, xmodule_instance_args)
return run_main_task(entry_id, task_fn, action_name)

View File

@@ -148,6 +148,49 @@ def _get_current_task():
return current_task
class TaskProgress(object):
"""
Encapsulates the current task's progress by keeping track of
'attempted', 'succeeded', 'skipped', 'failed', 'total',
'action_name', and 'duration_ms' values.
"""
def __init__(self, action_name, total, start_time):
self.action_name = action_name
self.total = total
self.start_time = start_time
self.attempted = 0
self.succeeded = 0
self.skipped = 0
self.failed = 0
def update_task_state(self, extra_meta=None):
"""
Update the current celery task's state to the progress state
specified by the current object. Returns the progress
dictionary for use by `run_main_task` and
`BaseInstructorTask.on_success`.
Arguments:
extra_meta (dict): Extra metadata to pass to `update_state`
Returns:
dict: The current task's progress dict
"""
progress_dict = {
'action_name': self.action_name,
'attempted': self.attempted,
'succeeded': self.succeeded,
'skipped': self.skipped,
'failed': self.failed,
'total': self.total,
'duration_ms': int((time() - self.start_time) * 1000),
}
if extra_meta is not None:
progress_dict.update(extra_meta)
_get_current_task().update_state(state=PROGRESS, meta=progress_dict)
return progress_dict
def run_main_task(entry_id, task_fcn, action_name):
"""
Applies the `task_fcn` to the arguments defined in `entry_id` InstructorTask.
@@ -243,9 +286,7 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta
result object.
"""
# get start time for task:
start_time = time()
usage_key = course_id.make_usage_key_from_deprecated_string(task_input.get('problem_url'))
student_identifier = task_input.get('student')
@@ -272,30 +313,11 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta
if filter_fcn is not None:
modules_to_update = filter_fcn(modules_to_update)
# perform the main loop
num_attempted = 0
num_succeeded = 0
num_skipped = 0
num_failed = 0
num_total = modules_to_update.count()
task_progress = TaskProgress(action_name, modules_to_update.count(), start_time)
task_progress.update_task_state()
def get_task_progress():
"""Return a dict containing info about current task"""
current_time = time()
progress = {'action_name': action_name,
'attempted': num_attempted,
'succeeded': num_succeeded,
'skipped': num_skipped,
'failed': num_failed,
'total': num_total,
'duration_ms': int((current_time - start_time) * 1000),
}
return progress
task_progress = get_task_progress()
_get_current_task().update_state(state=PROGRESS, meta=task_progress)
for module_to_update in modules_to_update:
num_attempted += 1
task_progress.attempted += 1
# There is no try here: if there's an error, we let it throw, and the task will
# be marked as FAILED, with a stack trace.
with dog_stats_api.timer('instructor_tasks.module.time.step', tags=[u'action:{name}'.format(name=action_name)]):
@@ -303,19 +325,15 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta
if update_status == UPDATE_STATUS_SUCCEEDED:
# If the update_fcn returns true, then it performed some kind of work.
# Logging of failures is left to the update_fcn itself.
num_succeeded += 1
task_progress.succeeded += 1
elif update_status == UPDATE_STATUS_FAILED:
num_failed += 1
task_progress.failed += 1
elif update_status == UPDATE_STATUS_SKIPPED:
num_skipped += 1
task_progress.skipped += 1
else:
raise UpdateProblemModuleStateError("Unexpected update_status returned: {}".format(update_status))
# update task status:
task_progress = get_task_progress()
_get_current_task().update_state(state=PROGRESS, meta=task_progress)
return task_progress
return task_progress.update_task_state()
def _get_task_id_from_xmodule_args(xmodule_instance_args):
@@ -505,7 +523,7 @@ def upload_csv_to_report_store(rows, csv_name, course_id, timestamp):
)
def push_grades_to_s3(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name):
def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name):
"""
For a given `course_id`, generate a grades CSV file for all students that
are enrolled, and store using a `ReportStore`. Once created, the files can
@@ -518,45 +536,26 @@ def push_grades_to_s3(_xmodule_instance_args, _entry_id, course_id, _task_input,
make a more general CSVDoc class instead of building out the rows like we
do here.
"""
start_time = datetime.now(UTC)
start_time = time()
start_date = datetime.now(UTC)
status_interval = 100
enrolled_students = CourseEnrollment.users_enrolled_in(course_id)
num_total = enrolled_students.count()
num_attempted = 0
num_succeeded = 0
num_failed = 0
curr_step = "Calculating Grades"
def update_task_progress():
"""Return a dict containing info about current task"""
current_time = datetime.now(UTC)
progress = {
'action_name': action_name,
'attempted': num_attempted,
'succeeded': num_succeeded,
'failed': num_failed,
'total': num_total,
'duration_ms': int((current_time - start_time).total_seconds() * 1000),
'step': curr_step,
}
_get_current_task().update_state(state=PROGRESS, meta=progress)
return progress
task_progress = TaskProgress(action_name, enrolled_students.count(), start_time)
# Loop over all our students and build our CSV lists in memory
header = None
rows = []
err_rows = [["id", "username", "error_msg"]]
current_step = {'step': 'Calculating Grades'}
for student, gradeset, err_msg in iterate_grades_for(course_id, enrolled_students):
# Periodically update task status (this is a cache write)
if num_attempted % status_interval == 0:
update_task_progress()
num_attempted += 1
if task_progress.attempted % status_interval == 0:
task_progress.update_task_state(extra_meta=current_step)
task_progress.attempted += 1
if gradeset:
# We were able to successfully grade this student for this course.
num_succeeded += 1
task_progress.succeeded += 1
if not header:
# Encode the header row in utf-8 encoding in case there are unicode characters
header = [section['label'].encode('utf-8') for section in gradeset[u'section_breakdown']]
@@ -578,37 +577,50 @@ def push_grades_to_s3(_xmodule_instance_args, _entry_id, course_id, _task_input,
rows.append([student.id, student.email, student.username, gradeset['percent']] + row_percents)
else:
# An empty gradeset means we failed to grade a student.
num_failed += 1
task_progress.failed += 1
err_rows.append([student.id, student.username, err_msg])
# By this point, we've got the rows we're going to stuff into our CSV files.
curr_step = "Uploading CSVs"
update_task_progress()
current_step = {'step': 'Uploading CSVs'}
task_progress.update_task_state(extra_meta=current_step)
# Perform the actual upload
upload_csv_to_report_store(rows, 'grade_report', course_id, start_time)
upload_csv_to_report_store(rows, 'grade_report', course_id, start_date)
# If there are any error rows (don't count the header), write them out as well
if len(err_rows) > 1:
upload_csv_to_report_store(err_rows, 'grade_report_err', course_id, start_time)
upload_csv_to_report_store(err_rows, 'grade_report_err', course_id, start_date)
# One last update before we close out...
return update_task_progress()
return task_progress.update_task_state(extra_meta=current_step)
def push_students_csv_to_s3(_xmodule_instance_args, _entry_id, course_id, task_input, _action_name):
def upload_students_csv(_xmodule_instance_args, _entry_id, course_id, task_input, action_name):
"""
For a given `course_id`, generate a CSV file containing profile
information for all students that are enrolled, and store using a
`ReportStore`.
"""
start_time = time()
start_date = datetime.now(UTC)
task_progress = TaskProgress(action_name, CourseEnrollment.num_enrolled_in(course_id), start_time)
current_step = {'step': 'Calculating Profile Info'}
task_progress.update_task_state(extra_meta=current_step)
# compute the student features table and format it
query_features = task_input.get('features')
student_data = enrolled_students_features(course_id, query_features)
header, rows = format_dictlist(student_data, query_features)
task_progress.attempted = task_progress.succeeded = len(rows)
task_progress.skipped = task_progress.total - task_progress.attempted
rows.insert(0, header)
# Perform the upload
upload_csv_to_report_store(rows, 'student_profile_info', course_id, datetime.now(UTC))
current_step = {'step': 'Uploading CSV'}
task_progress.update_task_state(extra_meta=current_step)
return UPDATE_STATUS_SUCCEEDED
# Perform the upload
upload_csv_to_report_store(rows, 'student_profile_info', course_id, start_date)
return task_progress.update_task_state(extra_meta=current_step)

View File

@@ -19,7 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from instructor_task.models import ReportStore
from instructor_task.tasks_helper import push_grades_to_s3, push_students_csv_to_s3, UPDATE_STATUS_SUCCEEDED
from instructor_task.tasks_helper import upload_grades_csv, upload_students_csv
class TestReport(ModuleStoreTestCase):
@@ -36,6 +36,7 @@ class TestReport(ModuleStoreTestCase):
def create_student(self, username, email):
student = UserFactory.create(username=username, email=email)
CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
return student
@ddt.ddt
@@ -55,9 +56,26 @@ class TestInstructorGradeReport(TestReport):
self.current_task.update_state = Mock()
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = self.current_task
result = push_grades_to_s3(None, None, self.course.id, None, 'graded')
#This assertion simply confirms that the generation completed with no errors
self.assertEquals(result['succeeded'], result['attempted'])
result = upload_grades_csv(None, None, self.course.id, None, 'graded')
num_students = len(emails)
self.assertDictContainsSubset({'attempted': num_students, 'succeeded': num_students, 'failed': 0}, result)
@patch('instructor_task.tasks_helper._get_current_task')
@patch('instructor_task.tasks_helper.iterate_grades_for')
def test_grading_failure(self, mock_iterate_grades_for, _mock_current_task):
"""
Test that any grading errors are properly reported in the
progress dict and uploaded to the report store.
"""
# mock an error response from `iterate_grades_for`
mock_iterate_grades_for.return_value = [
(self.create_student('username', 'student@example.com'), {}, 'Cannot grade student')
]
result = upload_grades_csv(None, None, self.course.id, None, 'graded')
self.assertDictContainsSubset({'attempted': 1, 'succeeded': 0, 'failed': 1}, result)
report_store = ReportStore.from_config()
self.assertTrue(any('grade_report_err' in item[0] for item in report_store.links_for(self.course.id)))
@ddt.ddt
@@ -66,14 +84,15 @@ class TestStudentReport(TestReport):
Tests that CSV student profile report generation works.
"""
def test_success(self):
self.create_student('student', 'student@example.com')
task_input = {'features': []}
with patch('instructor_task.tasks_helper._get_current_task'):
result = push_students_csv_to_s3(None, None, self.course.id, task_input, 'calculated')
result = upload_students_csv(None, None, self.course.id, task_input, 'calculated')
report_store = ReportStore.from_config()
links = report_store.links_for(self.course.id)
self.assertEquals(len(links), 1)
self.assertEquals(result, UPDATE_STATUS_SUCCEEDED)
self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result)
@ddt.data([u'student', u'student\xec'])
def test_unicode_usernames(self, students):
@@ -95,6 +114,7 @@ class TestStudentReport(TestReport):
}
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = self.current_task
result = push_students_csv_to_s3(None, None, self.course.id, task_input, 'calculated')
result = upload_students_csv(None, None, self.course.id, task_input, 'calculated')
#This assertion simply confirms that the generation completed with no errors
self.assertEquals(result, UPDATE_STATUS_SUCCEEDED)
num_students = len(students)
self.assertDictContainsSubset({'attempted': num_students, 'succeeded': num_students, 'failed': 0}, result)

View File

@@ -13,10 +13,27 @@ from xmodule.modulestore.django import modulestore
class CourseUpdatesList(generics.ListAPIView):
"""Notes:
"""
**Use Case**
1. This only works for new-style course updates and is not the older freeform
format.
Get the content for course updates.
**Example request**:
GET /api/mobile/v0.5/course_info/{organization}/{course_number}/{course_run}/updates
**Response Values**
A array of course updates. Each course update contains:
* date: The date of the course update.
* content: The content, as a string, of the course update. HTML tags
are not included in the string.
* status: Whether the update is visible or not.
* id: The unique identifier of the update.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
@@ -33,7 +50,18 @@ class CourseUpdatesList(generics.ListAPIView):
class CourseHandoutsList(generics.ListAPIView):
"""Please just render this in an HTML view for now.
"""
**Use Case**
Get the HTML for course handouts.
**Example request**:
GET /api/mobile/v0.5/course_info/{organization}/{course_number}/{course_run}/handouts
**Response Values**
* handouts_html: The HTML for course handouts.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
@@ -52,7 +80,17 @@ class CourseHandoutsList(generics.ListAPIView):
class CourseAboutDetail(generics.RetrieveAPIView):
"""
Renders course 'about' page
**Use Case**
Get the HTML for the course about page.
**Example request**:
GET /api/mobile/v0.5/course_info/{organization}/{course_number}/{course_run}/about
**Response Values**
* overview: The HTML for the course About page.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)

View File

@@ -23,10 +23,33 @@ class IsUser(permissions.BasePermission):
class UserDetail(generics.RetrieveAPIView):
"""Read-only information about our User.
"""
**Use Case**
This will be where users are redirected to after API login and will serve
as a place to list all useful resources this user can access.
Get information about the specified user and
access other resources the user has permissions for.
Users are redirected to this endpoint after logging in.
You can use the **course_enrollments** value in
the response to get a list of courses the user is enrolled in.
**Example request**:
GET /api/mobile/v0.5/users/{username}
**Response Values**
* id: The ID of the user.
* username: The username of the currently logged in user.
* email: The email address of the currently logged in user.
* name: The full name of the currently logged in user.
* course_enrollments: The URI to list the courses the currently logged
in user is enrolled in.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated, IsUser)
@@ -39,7 +62,38 @@ class UserDetail(generics.RetrieveAPIView):
class UserCourseEnrollmentsList(generics.ListAPIView):
"""Read-only list of courses that this user is enrolled in."""
"""
**Use Case**
Get information about the courses the currently logged in user is
enrolled in.
**Example request**:
GET /api/mobile/v0.5/users/{username}/course_enrollments/
**Response Values**
* created: The date the course was created.
* mode: The type of certificate registration for this course: honor or
certified.
* is_active: Whether the course is currently active; true or false.
* course: A collection of data about the course:
* course_about: The URI to get the data for the course About page.
* course_updates: The URI to get data for course updates.
* number: The course number.
* org: The organization that created the course.
* video_outline: The URI to get the list of all vides the user can
access in the course.
* id: The unique ID of the course.
* latest_updates: Reserved for future use.
* end: The end date of the course.
* name: The name of the course.
* course_handouts: The URI to get data for course handouts.
* start: The data and time the course starts.
* course_image: The path to the course image.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated, IsUser)
queryset = CourseEnrollment.objects.all()

View File

@@ -25,7 +25,58 @@ from .serializers import BlockOutline, video_summary
class VideoSummaryList(generics.ListAPIView):
"""A list of all Videos in this Course that the user has access to."""
"""
**Use Case**
Get a list of all videos in the specified course. You can use the
video_url value to access the video file.
**Example request**:
GET /api/mobile/v0.5/video_outlines/courses/{organization}/{course_number}/{course_run}
**Response Values**
An array of videos in the course. For each video:
* section_url: The URL to the first page of the section that
contains the video in the Learning Managent System.
* path: An array containing category and name values specifying the
complete path the the video in the courseware hierarcy. The
following categories values are included: "chapter", "sequential",
and "vertical". The name value is the display name for that object.
* unit_url: The URL to the unit contains the video in the Learning
Managent System.
* named_path: An array consisting of the display names of the
courseware objects in the path to the video.
* summary: An array of data about the video that includes:
* category: The type of component, in this case always "video".
* video_thumbnail_url: The URL to the thumbnail image for the
video, if available.
* language: The language code for the video.
* name: The display name of the video.
* video_url: The URL to the video file. Use this value to access
the video.
* duration: The length of the video, if available.
* transcripts: An array of language codes and URLs to available
video transcripts. Use the URL value to access a transcript
for the video.
* id: The unique identifier for the video.
* size: The size of the video file
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
@@ -45,9 +96,19 @@ class VideoSummaryList(generics.ListAPIView):
class VideoTranscripts(generics.RetrieveAPIView):
"""Read-only view for a single transcript (SRT) file for a particular language.
"""
**Use Case**
Use to get a transcript for a specified video and language.
**Example request**:
GET /api/mobile/v0.5/video_outlines/transcripts/{organization}/{course_number}/{course_run}/{video ID}/{language code}
**Response Values**
An HttpResponse with an SRT file download.
Returns an `HttpResponse` with an SRT file download for the body.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)

View File

@@ -2,8 +2,10 @@
""" Tests for student account views. """
import re
from unittest import skipUnless
from urllib import urlencode
import json
from mock import patch
import ddt
from django.test import TestCase
@@ -14,6 +16,7 @@ from django.core import mail
from util.testing import UrlResetMixin
from user_api.api import account as account_api
from user_api.api import profile as profile_api
from util.bad_request_rate_limiter import BadRequestRateLimiter
@ddt.ddt
@@ -22,10 +25,13 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
USERNAME = u"heisenberg"
ALTERNATE_USERNAME = u"walt"
PASSWORD = u"ḅḷüëṡḳÿ"
OLD_PASSWORD = u"ḅḷüëṡḳÿ"
NEW_PASSWORD = u"🄱🄸🄶🄱🄻🅄🄴"
OLD_EMAIL = u"walter@graymattertech.com"
NEW_EMAIL = u"walt@savewalterwhite.com"
INVALID_ATTEMPTS = 100
INVALID_EMAILS = [
None,
u"",
@@ -50,11 +56,11 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
super(StudentAccountViewTest, self).setUp("student_account.urls")
# Create/activate a new account
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.OLD_EMAIL)
activation_key = account_api.create_account(self.USERNAME, self.OLD_PASSWORD, self.OLD_EMAIL)
account_api.activate_account(activation_key)
# Login
result = self.client.login(username=self.USERNAME, password=self.PASSWORD)
result = self.client.login(username=self.USERNAME, password=self.OLD_PASSWORD)
self.assertTrue(result)
def test_index(self):
@@ -93,7 +99,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self.assertContains(response, expected_data)
def test_change_email(self):
response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
response = self._change_email(self.NEW_EMAIL, self.OLD_PASSWORD)
self.assertEquals(response.status_code, 200)
# Verify that the email associated with the account remains unchanged
@@ -105,8 +111,8 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self._assert_email(
mail.outbox[0],
[self.NEW_EMAIL],
u'Email Change Request',
u'There was recently a request to change the email address'
u"Email Change Request",
u"There was recently a request to change the email address"
)
# Retrieve the activation key from the email
@@ -128,48 +134,48 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self._assert_email(
mail.outbox[1],
[self.OLD_EMAIL, self.NEW_EMAIL],
u'Email Change Successful',
u'You successfully changed the email address'
u"Email Change Successful",
u"You successfully changed the email address"
)
def test_email_change_wrong_password(self):
response = self._change_email(self.NEW_EMAIL, "wrong password")
self.assertEqual(response.status_code, 401)
def test_email_change_request_internal_error(self):
def test_email_change_request_no_user(self):
# Patch account API to raise an internal error when an email change is requested
with patch('student_account.views.account_api.request_email_change') as mock_call:
mock_call.side_effect = account_api.AccountUserNotFound
response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
response = self._change_email(self.NEW_EMAIL, self.OLD_PASSWORD)
self.assertEquals(response.status_code, 500)
self.assertEquals(response.status_code, 400)
def test_email_change_request_email_taken_by_active_account(self):
# Create/activate a second user with the new email
activation_key = account_api.create_account(self.ALTERNATE_USERNAME, self.PASSWORD, self.NEW_EMAIL)
activation_key = account_api.create_account(self.ALTERNATE_USERNAME, self.OLD_PASSWORD, self.NEW_EMAIL)
account_api.activate_account(activation_key)
# Request to change the original user's email to the email now used by the second user
response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
response = self._change_email(self.NEW_EMAIL, self.OLD_PASSWORD)
self.assertEquals(response.status_code, 409)
def test_email_change_request_email_taken_by_inactive_account(self):
# Create a second user with the new email, but don't active them
account_api.create_account(self.ALTERNATE_USERNAME, self.PASSWORD, self.NEW_EMAIL)
account_api.create_account(self.ALTERNATE_USERNAME, self.OLD_PASSWORD, self.NEW_EMAIL)
# Request to change the original user's email to the email used by the inactive user
response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
response = self._change_email(self.NEW_EMAIL, self.OLD_PASSWORD)
self.assertEquals(response.status_code, 200)
@ddt.data(*INVALID_EMAILS)
def test_email_change_request_email_invalid(self, invalid_email):
# Request to change the user's email to an invalid address
response = self._change_email(invalid_email, self.PASSWORD)
response = self._change_email(invalid_email, self.OLD_PASSWORD)
self.assertEquals(response.status_code, 400)
def test_email_change_confirmation(self):
# Get an email change activation key
activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.PASSWORD)
activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.OLD_PASSWORD)
# Follow the link sent in the confirmation email
response = self.client.get(reverse('email_change_confirm', kwargs={'key': activation_key}))
@@ -190,10 +196,10 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
def test_email_change_confirmation_email_already_exists(self):
# Get an email change activation key
email_activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.PASSWORD)
email_activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.OLD_PASSWORD)
# Create/activate a second user with the new email
account_activation_key = account_api.create_account(self.ALTERNATE_USERNAME, self.PASSWORD, self.NEW_EMAIL)
account_activation_key = account_api.create_account(self.ALTERNATE_USERNAME, self.OLD_PASSWORD, self.NEW_EMAIL)
account_api.activate_account(account_activation_key)
# Follow the link sent to the original user
@@ -206,7 +212,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
def test_email_change_confirmation_internal_error(self):
# Get an email change activation key
activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.PASSWORD)
activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.OLD_PASSWORD)
# Patch account API to return an internal error
with patch('student_account.views.account_api.confirm_email_change') as mock_call:
@@ -215,14 +221,120 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self.assertContains(response, "Something went wrong")
def test_change_email_request_missing_email_param(self):
response = self._change_email(None, self.PASSWORD)
def test_email_change_request_missing_email_param(self):
response = self._change_email(None, self.OLD_PASSWORD)
self.assertEqual(response.status_code, 400)
def test_change_email_request_missing_password_param(self):
def test_email_change_request_missing_password_param(self):
response = self._change_email(self.OLD_EMAIL, None)
self.assertEqual(response.status_code, 400)
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
def test_password_change(self):
# Request a password change while logged in, simulating
# use of the password reset link from the account page
response = self._change_password()
self.assertEqual(response.status_code, 200)
# Check that an email was sent
self.assertEqual(len(mail.outbox), 1)
# Retrieve the activation link from the email body
email_body = mail.outbox[0].body
result = re.search('(?P<url>https?://[^\s]+)', email_body)
self.assertIsNot(result, None)
activation_link = result.group('url')
# Visit the activation link
response = self.client.get(activation_link)
self.assertEqual(response.status_code, 200)
# Submit a new password and follow the redirect to the success page
response = self.client.post(
activation_link,
# These keys are from the form on the current password reset confirmation page.
{'new_password1': self.NEW_PASSWORD, 'new_password2': self.NEW_PASSWORD},
follow=True
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Your password has been set.")
# Log the user out to clear session data
self.client.logout()
# Verify that the new password can be used to log in
result = self.client.login(username=self.USERNAME, password=self.NEW_PASSWORD)
self.assertTrue(result)
# Try reusing the activation link to change the password again
response = self.client.post(
activation_link,
{'new_password1': self.OLD_PASSWORD, 'new_password2': self.OLD_PASSWORD},
follow=True
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "The password reset link was invalid, possibly because the link has already been used.")
self.client.logout()
# Verify that the old password cannot be used to log in
result = self.client.login(username=self.USERNAME, password=self.OLD_PASSWORD)
self.assertFalse(result)
# Verify that the new password continues to be valid
result = self.client.login(username=self.USERNAME, password=self.NEW_PASSWORD)
self.assertTrue(result)
@ddt.data(True, False)
def test_password_change_logged_out(self, send_email):
# Log the user out
self.client.logout()
# Request a password change while logged out, simulating
# use of the password reset link from the login page
if send_email:
response = self._change_password(email=self.OLD_EMAIL)
self.assertEqual(response.status_code, 200)
else:
# Don't send an email in the POST data, simulating
# its (potentially accidental) omission in the POST
# data sent from the login page
response = self._change_password()
self.assertEqual(response.status_code, 400)
def test_password_change_inactive_user(self):
# Log out the user created during test setup
self.client.logout()
# Create a second user, but do not activate it
account_api.create_account(self.ALTERNATE_USERNAME, self.OLD_PASSWORD, self.NEW_EMAIL)
# Send the view the email address tied to the inactive user
response = self._change_password(email=self.NEW_EMAIL)
self.assertEqual(response.status_code, 400)
def test_password_change_no_user(self):
# Log out the user created during test setup
self.client.logout()
# Send the view an email address not tied to any user
response = self._change_password(email=self.NEW_EMAIL)
self.assertEqual(response.status_code, 400)
def test_password_change_rate_limited(self):
# Log out the user created during test setup, to prevent the view from
# selecting the logged-in user's email address over the email provided
# in the POST data
self.client.logout()
# Make many consecutive bad requests in an attempt to trigger the rate limiter
for attempt in xrange(self.INVALID_ATTEMPTS):
self._change_password(email=self.NEW_EMAIL)
response = self._change_password(email=self.NEW_EMAIL)
self.assertEqual(response.status_code, 403)
@ddt.data(
('get', 'account_index', []),
('post', 'email_change_request', []),
@@ -242,7 +354,8 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
@ddt.data(
('get', 'account_index', []),
('post', 'email_change_request', []),
('get', 'email_change_confirm', [123])
('get', 'email_change_confirm', [123]),
('post', 'password_change_request', []),
)
@ddt.unpack
def test_require_http_method(self, correct_method, url_name, args):
@@ -270,3 +383,12 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
data['password'] = password.encode('utf-8')
return self.client.post(path=reverse('email_change_request'), data=data)
def _change_password(self, email=None):
"""Request to change the user's password. """
data = {}
if email:
data['email'] = email
return self.client.post(path=reverse('password_change_request'), data=data)

View File

@@ -14,4 +14,5 @@ if settings.FEATURES.get('ENABLE_NEW_DASHBOARD'):
url(r'^$', 'index', name='account_index'),
url(r'^email$', 'email_change_request_handler', name='email_change_request'),
url(r'^email/confirmation/(?P<key>[^/]*)$', 'email_change_confirmation_handler', name='email_change_confirm'),
)
url(r'^password$', 'password_change_request_handler', name='password_change_request'),
)

View File

@@ -1,9 +1,10 @@
""" Views for a student's account information. """
import logging
import json
from django.conf import settings
from django.http import (
HttpResponse, HttpResponseBadRequest, HttpResponseServerError
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
)
from django.core.mail import send_mail
from django_future.csrf import ensure_csrf_cookie
@@ -15,6 +16,10 @@ from microsite_configuration import microsite
from user_api.api import account as account_api
from user_api.api import profile as profile_api
from util.bad_request_rate_limiter import BadRequestRateLimiter
AUDIT_LOG = logging.getLogger("audit")
@login_required
@@ -80,18 +85,20 @@ def login_and_registration_form(request, initial_mode="login"):
def email_change_request_handler(request):
"""Handle a request to change the user's email address.
Sends an email to the newly specified address containing a link
to a confirmation page.
Args:
request (HttpRequest)
Returns:
HttpResponse: 200 if the confirmation email was sent successfully
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 400 if the format of the new email is incorrect
HttpResponse: 400 if the format of the new email is incorrect, or if
an email change is requested for a user which does not exist
HttpResponse: 401 if the provided password (in the form) is incorrect
HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 409 if the provided email is already in use
HttpResponse: 500 if the user to which the email change will be applied
does not exist
Example usage:
@@ -111,12 +118,10 @@ def email_change_request_handler(request):
try:
key = account_api.request_email_change(username, new_email, password)
except account_api.AccountUserNotFound:
return HttpResponseServerError()
except (account_api.AccountEmailInvalid, account_api.AccountUserNotFound):
return HttpResponseBadRequest()
except account_api.AccountEmailAlreadyExists:
return HttpResponse(status=409)
except account_api.AccountEmailInvalid:
return HttpResponseBadRequest()
except account_api.AccountNotAuthorized:
return HttpResponse(status=401)
@@ -138,7 +143,6 @@ def email_change_request_handler(request):
# Send a confirmation email to the new address containing the activation key
send_mail(subject, message, from_address, [new_email])
# Send a 200 response code to the client to indicate that the email was sent successfully.
return HttpResponse(status=200)
@@ -155,15 +159,15 @@ def email_change_confirmation_handler(request, key):
Returns:
HttpResponse: 200 if the email change is successful, the activation key
is invalid, the new email is already in use, or the
user to which the email change will be applied does
not exist
is invalid, the new email is already in use, or the
user to which the email change will be applied does
not exist
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 405 if using an unsupported HTTP method
Example usage:
GET /account/email_change_confirm/{key}
GET /account/email/confirmation/{key}
"""
try:
@@ -212,3 +216,53 @@ def email_change_confirmation_handler(request, key):
'disable_courseware_js': True,
}
)
@require_http_methods(['POST'])
def password_change_request_handler(request):
"""Handle password change requests originating from the account page.
Uses the Account API to email the user a link to the password reset page.
Note:
The next step in the password reset process (confirmation) is currently handled
by student.views.password_reset_confirm_wrapper, a custom wrapper around Django's
password reset confirmation view.
Args:
request (HttpRequest)
Returns:
HttpResponse: 200 if the email was sent successfully
HttpResponse: 400 if there is no 'email' POST parameter, or if no user with
the provided email exists
HttpResponse: 403 if the client has been rate limited
HttpResponse: 405 if using an unsupported HTTP method
Example usage:
POST /account/password
"""
limiter = BadRequestRateLimiter()
if limiter.is_rate_limit_exceeded(request):
AUDIT_LOG.warning("Password reset rate limit exceeded")
return HttpResponseForbidden()
user = request.user
# Prefer logged-in user's email
email = user.email if user.is_authenticated() else request.POST.get('email')
if email:
try:
account_api.request_password_change(email, request.get_host(), request.is_secure())
except account_api.AccountUserNotFound:
AUDIT_LOG.info("Invalid password reset attempt")
# Increment the rate limit counter
limiter.tick_bad_request_counter(request)
return HttpResponseBadRequest("No active user with the provided email address exists.")
return HttpResponse(status=200)
else:
return HttpResponseBadRequest("No email address provided.")

View File

@@ -99,12 +99,7 @@ class TestProfEdVerification(ModuleStoreTestCase):
# On the verified page, expect that there's a link to payment page
self.assertContains(resp, '/shoppingcart/payment_fake')
def test_do_not_auto_register(self):
# TODO (ECOM-16): Remove once we complete the AB-test of auto-registration.
session = self.client.session
session['auto_register'] = True
session.save()
def test_do_not_auto_enroll(self):
# Go to the course mode page, expecting a redirect
# to the show requirements page.
resp = self.client.get(self.urls['course_modes_choose'], follow=True)

View File

@@ -191,7 +191,7 @@ class TestVerifyView(ModuleStoreTestCase):
kwargs={"course_id": unicode(self.course_key)})
response = self.client.get(url)
self.assertIn("You are registering for", response.content)
self.assertIn("You are now registered to audit", response.content)
def test_valid_course_upgrade_text(self):
url = reverse('verify_student_verify',

View File

@@ -113,9 +113,6 @@ class VerifyView(View):
"upgrade": upgrade == u'True',
"can_audit": CourseMode.mode_for_course(course_id, 'audit') is not None,
"modes_dict": CourseMode.modes_for_course_dict(course_id),
# TODO (ECOM-16): Remove once the AB test completes
"autoreg": request.session.get('auto_register', False),
"retake": request.GET.get('retake', False),
}
@@ -166,9 +163,6 @@ class VerifiedView(View):
"upgrade": upgrade == u'True',
"can_audit": "audit" in modes_dict,
"modes_dict": modes_dict,
# TODO (ECOM-16): Remove once the AB test completes
"autoreg": request.session.get('auto_register', False),
}
return render_to_response('verify_student/verified.html', context)
@@ -358,9 +352,6 @@ def show_requirements(request, course_id):
"is_not_active": not request.user.is_active,
"upgrade": upgrade == u'True',
"modes_dict": modes_dict,
# TODO (ECOM-16): Remove once the AB test completes
"autoreg": request.session.get('auto_register', False),
}
return render_to_response("verify_student/show_requirements.html", context)

View File

@@ -467,9 +467,6 @@ OPTIMIZELY_PROJECT_ID = AUTH_TOKENS.get('OPTIMIZELY_PROJECT_ID', OPTIMIZELY_PROJ
#### Course Registration Code length ####
REGISTRATION_CODE_LENGTH = ENV_TOKENS.get('REGISTRATION_CODE_LENGTH', 8)
# TODO (ECOM-16): Remove once the A/B test of auto-registration completes
AUTO_REGISTRATION_AB_TEST_EXCLUDE_COURSES = set(ENV_TOKENS.get('AUTO_REGISTRATION_AB_TEST_EXCLUDE_COURSES', AUTO_REGISTRATION_AB_TEST_EXCLUDE_COURSES))
# REGISTRATION CODES DISPLAY INFORMATION
INVOICE_CORP_ADDRESS = ENV_TOKENS.get('INVOICE_CORP_ADDRESS', INVOICE_CORP_ADDRESS)
INVOICE_PAYMENT_INSTRUCTIONS = ENV_TOKENS.get('INVOICE_PAYMENT_INSTRUCTIONS', INVOICE_PAYMENT_INSTRUCTIONS)

View File

@@ -75,11 +75,6 @@
},
"FEEDBACK_SUBMISSION_EMAIL": "",
"GITHUB_REPO_ROOT": "** OVERRIDDEN **",
"GRADES_DOWNLOAD": {
"BUCKET": "edx-grades",
"ROOT_PATH": "/tmp/edx-s3/grades",
"STORAGE_TYPE": "localfs"
},
"LMS_BASE": "localhost:8003",
"LOCAL_LOGLEVEL": "INFO",
"LOGGING_ENV": "sandbox",

View File

@@ -4,7 +4,7 @@ Settings for bok choy tests
import os
from path import path
from tempfile import mkdtemp
CONFIG_ROOT = path(__file__).abspath().dirname() # pylint: disable=E1120
TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root"
@@ -42,6 +42,13 @@ update_module_store_settings(
default_store=os.environ.get('DEFAULT_STORE', 'draft'),
)
###################### Grade Downloads ######################
GRADES_DOWNLOAD = {
'STORAGE_TYPE': 'localfs',
'BUCKET': 'edx-grades',
'ROOT_PATH': os.path.join(mkdtemp(), 'edx-s3', 'grades'),
}
# Configure the LMS to use our stub XQueue implementation
XQUEUE_INTERFACE['url'] = 'http://localhost:8040'

View File

@@ -27,14 +27,13 @@ Longer TODO:
import sys
import os
import imp
import json
from path import path
from warnings import simplefilter
from django.utils.translation import ugettext_lazy as _
from .discussionsettings import *
from xmodule.modulestore.modulestore_settings import update_module_store_settings
from lms.lib.xblock.mixin import LmsBlockMixin
################################### FEATURES ###################################
@@ -273,15 +272,6 @@ FEATURES = {
# Default to false here b/c dev environments won't have the api, will override in aws.py
'ENABLE_ANALYTICS_ACTIVE_COUNT': False,
# TODO: ECOM-136 remove this feature flag when new styles are available on main site.for
# Enable the new edX footer to be rendered. Defaults to false.
'ENABLE_NEW_EDX_FOOTER': False,
# TODO: ECOM-136
# Enables the new navigation template and styles. This should be enabled
# when the styles appropriately match the edX.org website.
'ENABLE_NEW_EDX_HEADER': False,
# When a logged in user goes to the homepage ('/') should the user be
# redirected to the dashboard - this is default Open edX behavior. Set to
# False to not redirect the user
@@ -410,9 +400,6 @@ TEMPLATE_CONTEXT_PROCESSORS = (
# Allows the open edX footer to be leveraged in Django Templates.
'edxmako.shortcuts.open_source_footer_context_processor',
# TODO: Used for header and footer feature flags. Remove as part of ECOM-136
'edxmako.shortcuts.header_footer_context_processor',
# Shoppingcart processor (detects if request.user has a cart)
'shoppingcart.context_processor.user_has_cart_context_processor',
@@ -684,11 +671,14 @@ FAVICON_PATH = 'images/favicon.ico'
# Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
# these languages display right to left
LANGUAGES_BIDI = ("en@rtl", "he", "ar", "fa", "ur", "fa-ir")
# Sourced from http://www.localeplanet.com/icu/ and wikipedia
LANGUAGES = (
('en', u'English'),
('eo', u'Dummy Language (Esperanto)'), # Dummy language used for testing
('en@rtl', u'English (right-to-left)'),
('eo', u'Dummy Language (Esperanto)'), # Dummy languaged used for testing
('fake2', u'Fake translations'), # Another dummy language for testing (not pushed to prod)
('am', u'አማርኛ'), # Amharic
@@ -1079,6 +1069,25 @@ PIPELINE_CSS = {
],
'output_filename': 'css/lms-style-app-extend2.css',
},
'style-app-rtl': {
'source_filenames': [
'sass/application-rtl.css',
'sass/ie-rtl.css'
],
'output_filename': 'css/lms-style-app-rtl.css',
},
'style-app-extend1-rtl': {
'source_filenames': [
'sass/application-extend1-rtl.css',
],
'output_filename': 'css/lms-style-app-extend1-rtl.css',
},
'style-app-extend2-rtl': {
'source_filenames': [
'sass/application-extend2-rtl.css',
],
'output_filename': 'css/lms-style-app-extend2-rtl.css',
},
'style-course-vendor': {
'source_filenames': [
'js/vendor/CodeMirror/codemirror.css',
@@ -1094,6 +1103,13 @@ PIPELINE_CSS = {
],
'output_filename': 'css/lms-style-course.css',
},
'style-course-rtl': {
'source_filenames': [
'sass/course-rtl.css',
'xmodule/modules.css',
],
'output_filename': 'css/lms-style-course-rtl.css',
},
'style-xmodule-annotations': {
'source_filenames': [
'css/vendor/ova/annotator.css',
@@ -1817,24 +1833,12 @@ ANALYTICS_DATA_TOKEN = ""
ANALYTICS_DASHBOARD_URL = ""
ANALYTICS_DASHBOARD_NAME = PLATFORM_NAME + " Insights"
# TODO (ECOM-16): Remove once the A/B test of auto-registration completes
AUTO_REGISTRATION_AB_TEST_EXCLUDE_COURSES = set([
"HarvardX/SW12.2x/1T2014",
"HarvardX/SW12.3x/1T2014",
"HarvardX/SW12.4x/1T2014",
"HarvardX/SW12.5x/2T2014",
"HarvardX/SW12.6x/2T2014",
"HarvardX/HUM2.1x/3T2014",
"HarvardX/SW12x/2013_SOND",
"LinuxFoundationX/LFS101x/2T2014",
"HarvardX/CS50x/2014_T1",
"HarvardX/AmPoX.1/2014_T3",
"HarvardX/SW12.7x/3T2014",
"HarvardX/SW12.10x/1T2015",
"HarvardX/SW12.9x/3T2014",
"HarvardX/SW12.8x/3T2014",
])
# REGISTRATION CODES DISPLAY INFORMATION SUBTITUTIONS IN THE INVOICE ATTACHMENT
INVOICE_CORP_ADDRESS = "Please place your corporate address\nin this configuration"
INVOICE_PAYMENT_INSTRUCTIONS = "This is where you can\nput directions on how people\nbuying registration codes"
# Country code overrides
# Used by django-countries
COUNTRIES_OVERRIDE = {
"TW": _("Taiwan"),
}

View File

@@ -178,7 +178,7 @@ CACHES = {
'mongo_metadata_inheritance': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'LOCATION': os.path.join(tempfile.gettempdir(), 'mongo_metadata_inheritance'),
'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key',
},

View File

@@ -8,7 +8,7 @@ class User(models.Model):
accessible_fields = ['username', 'follower_ids', 'upvoted_ids', 'downvoted_ids',
'id', 'external_id', 'subscribed_user_ids', 'children', 'course_id',
'subscribed_thread_ids', 'subscribed_commentable_ids',
'group_id', 'subscribed_thread_ids', 'subscribed_commentable_ids',
'subscribed_course_ids', 'threads_count', 'comments_count',
'default_sort_key'
]
@@ -120,6 +120,8 @@ class User(models.Model):
retrieve_params.update(kwargs)
if self.attributes.get('course_id'):
retrieve_params['course_id'] = self.course_id.to_deprecated_string()
if self.attributes.get('group_id'):
retrieve_params['group_id'] = self.group_id
try:
response = perform_request(
'get',

View File

@@ -20,11 +20,6 @@ def run():
"""
Executed during django startup
"""
# Patch the xml libs.
from safe_lxml import defuse_xml_libs
defuse_xml_libs()
django_utils_translation.patch()
autostartup()

View File

@@ -153,7 +153,7 @@ class ReportDownloads
@$report_downloads_table = @$section.find ".report-downloads-table"
POLL_INTERVAL = 1000 * 60 * 5 # 5 minutes in ms
POLL_INTERVAL = 20000 # 20 seconds, just like the "pending instructor tasks" table
@downloads_poller = new window.InstructorDashboard.util.IntervalManager(
POLL_INTERVAL, => @reload_report_downloads()
)

View File

@@ -0,0 +1,224 @@
/**
* Legacy JavaScript for the student dashboard.
* Please do not add anything else to this file unless
* you have an extremely good reason. New JavaScript
* for the dashboard should be implemented as self-contained
* modules with unit tests.
*/
var edx = edx || {};
(function($, gettext, Logger, accessibleModal) {
'use strict';
edx.dashboard = edx.dashboard || {};
edx.dashboard.legacy = {};
/**
* Initialize the dashboard using legacy JavaScript.
*
* @param{Object} urls - The URLs used by the JavaScript,
* which are generated by the server and passed into
* this function by the rendered page.
*
* Specifically:
* - dashboard
* - signInUser
* - passwordReset
* - changeEmail
* - changeEmailSettings
* - changeName
* - verifyToggleBannerFailedOff
*/
edx.dashboard.legacy.init = function(urls) {
// On initialization, set focus to the first notification available
// for screen readers.
var notifications = $('.dashboard-notifications');
if (notifications.children().length > 0) {
notifications.focus();
}
$('.message.is-expandable .wrapper-tip').bind('click', toggleExpandMessage);
function toggleExpandMessage(e) {
(e).preventDefault();
$(this).closest('.message.is-expandable').toggleClass('is-expanded');
var course = $("#upgrade-to-verified").data("course-id");
analytics.track('edx.bi.dashboard.upsell_copy.clicked', {
category: 'user-engagement',
label: course
});
}
$("#failed-verification-button-dismiss").click(function() {
$.ajax({
url: urls.verifyToggleBannerFailedOff,
type: "post"
});
$("#failed-verification-banner").addClass('is-hidden');
});
$("#upgrade-to-verified").click(function(event) {
var user = $(event.target).data("user");
var course = $(event.target).data("course-id");
Logger.log('edx.course.enrollment.upgrade.clicked', [user, course], null);
});
$(".email-settings").click(function(event) {
$("#email_settings_course_id").val( $(event.target).data("course-id") );
$("#email_settings_course_number").text( $(event.target).data("course-number") );
if($(event.target).data("optout") === "False") {
$("#receive_emails").prop('checked', true);
}
});
$(".unenroll").click(function(event) {
$("#unenroll_course_id").val( $(event.target).data("course-id") );
$("#unenroll_course_number").text( $(event.target).data("course-number") );
});
$('#unenroll_form').on('ajax:complete', function(event, xhr) {
if(xhr.status === 200) {
location.href = urls.dashboard;
} else if (xhr.status === 403) {
location.href = urls.signInUser + "?course_id=" +
encodeURIComponent($("#unenroll_course_id").val()) + "&enrollment_action=unenroll";
} else {
$('#unenroll_error').html(
xhr.responseText ? xhr.responseText : gettext("An error occurred. Please try again later.")
).stop().css("display", "block");
}
});
$('#pwd_reset_button').click(function() {
$.post(
urls.passwordReset,
{"email" : $('#id_email').val()},
function() {
$("#password_reset_complete_link").click();
}
);
});
$("#submit-lang").click(function(event) {
event.preventDefault();
$.post('/lang_pref/setlang/',
{language: $('#settings-language-value').val()}
).done(function() {
// submit form as normal
$('.settings-language-form').submit();
});
});
$("#change_email_form").submit(function(){
var new_email = $('#new_email_field').val();
var new_password = $('#new_email_password').val();
$.post(
urls.changeEmail,
{"new_email" : new_email, "password" : new_password},
function(data) {
if (data.success) {
$("#change_email_title").html(gettext("Please verify your new email address"));
$("#change_email_form").html(
"<p>" +
gettext("You'll receive a confirmation in your inbox. Please follow the link in the email to confirm your email address change.") +
"</p>"
);
} else {
$("#change_email_error").html(data.error).stop().css("display", "block");
}
}
);
return false;
});
$("#change_name_form").submit(function(){
var new_name = $('#new_name_field').val();
var rationale = $('#name_rationale_field').val();
$.post(
urls.changeName,
{"new_name":new_name, "rationale":rationale},
function(data) {
if(data.success) {
location.reload();
} else {
$("#change_name_error").html(data.error).stop().css("display", "block");
}
}
);
return false;
});
$("#email_settings_form").submit(function(){
$.ajax({
type: "POST",
url: urls.changeEmailSettings,
data: $(this).serializeArray(),
success: function(data) {
if(data.success) {
location.href = urls.dashboard;
}
},
error: function(xhr) {
if (xhr.status === 403) {
location.href = urls.signInUser;
}
}
});
return false;
});
accessibleModal(
".edit-name",
"#apply_name_change .close-modal",
"#apply_name_change",
"#dashboard-main"
);
accessibleModal(
".edit-email",
"#change_email .close-modal",
"#change_email",
"#dashboard-main"
);
accessibleModal(
"#pwd_reset_button",
"#password_reset_complete .close-modal",
"#password_reset_complete",
"#dashboard-main"
);
$(".email-settings").each(function(index){
$(this).attr("id", "unenroll-" + index);
// a bit of a hack, but gets the unique selector for the modal trigger
var trigger = "#" + $(this).attr("id");
accessibleModal(
trigger,
"#email-settings-modal .close-modal",
"#email-settings-modal",
"#dashboard-main"
);
});
$(".unenroll").each(function(index){
$(this).attr("id", "email-settings-" + index);
// a bit of a hack, but gets the unique selector for the modal trigger
var trigger = "#" + $(this).attr("id");
accessibleModal(
trigger,
"#unenroll-modal .close-modal",
"#unenroll-modal",
"#dashboard-main"
);
});
$("#unregister_block_course").click( function(event) {
$("#unenroll_course_id").val($(event.target).data("course-id"));
$("#unenroll_course_number").text($(event.target).data("course-number"));
});
};
})(jQuery, gettext, Logger, accessible_modal);

View File

@@ -97,6 +97,11 @@ define(['js/student_account/account'],
view.submit(fakeEvent);
};
var requestPasswordChange = function() {
var fakeEvent = {preventDefault: function() {}};
view.click(fakeEvent);
};
var assertAjax = function(url, method, data) {
expect($.ajax).toHaveBeenCalled();
var ajaxArgs = $.ajax.mostRecentCall.args[0];
@@ -106,31 +111,13 @@ define(['js/student_account/account'],
expect(ajaxArgs.headers.hasOwnProperty("X-CSRFToken")).toBe(true);
};
var assertEmailStatus = function(success, expectedStatus) {
var assertStatus = function(selection, success, errorClass, expectedStatus) {
if (!success) {
expect(view.$emailStatus).toHaveClass("validation-error");
expect(selection).toHaveClass(errorClass);
} else {
expect(view.$emailStatus).not.toHaveClass("validation-error");
expect(selection).not.toHaveClass(errorClass);
}
expect(view.$emailStatus.text()).toEqual(expectedStatus);
};
var assertPasswordStatus = function(success, expectedStatus) {
if (!success) {
expect(view.$passwordStatus).toHaveClass("validation-error");
} else {
expect(view.$passwordStatus).not.toHaveClass("validation-error");
}
expect(view.$passwordStatus.text()).toEqual(expectedStatus);
};
var assertRequestStatus = function(success, expectedStatus) {
if (!success) {
expect(view.$requestStatus).toHaveClass("error");
} else {
expect(view.$requestStatus).not.toHaveClass("error");
}
expect(view.$requestStatus.text()).toEqual(expectedStatus);
expect(selection.text()).toEqual(expectedStatus);
};
beforeEach(function() {
@@ -139,7 +126,7 @@ define(['js/student_account/account'],
view = new edx.student.account.AccountView().render();
// Stub Ajax cals to return success/failure
// Stub Ajax calls to return success/failure
spyOn($, "ajax").andCallFake(function() {
return $.Deferred(function(defer) {
if (ajaxSuccess) {
@@ -157,39 +144,57 @@ define(['js/student_account/account'],
email: "bob@example.com",
password: "password"
});
assertRequestStatus(true, "Please check your email to confirm the change");
assertStatus(view.$requestStatus, true, "error", "Please check your email to confirm the change");
});
it("displays email validation errors", function() {
// Invalid email should display an error
requestEmailChange("invalid", "password");
assertEmailStatus(false, "Please enter a valid email address");
assertStatus(view.$emailStatus, false, "validation-error", "Please enter a valid email address");
// Once the error is fixed, the status should return to normal
requestEmailChange("bob@example.com", "password");
assertEmailStatus(true, "");
assertStatus(view.$emailStatus, true, "validation-error", "");
});
it("displays an invalid password error", function() {
// Password cannot be empty
requestEmailChange("bob@example.com", "");
assertPasswordStatus(false, "Please enter a valid password");
assertStatus(view.$passwordStatus, false, "validation-error", "Please enter a valid password");
// Once the error is fixed, the status should return to normal
requestEmailChange("bob@example.com", "password");
assertPasswordStatus(true, "");
assertStatus(view.$passwordStatus, true, "validation-error", "");
});
it("displays server errors", function() {
// Simulate an error from the server
ajaxSuccess = false;
requestEmailChange("bob@example.com", "password");
assertRequestStatus(false, "The data could not be saved.");
assertStatus(view.$requestStatus, false, "error", "The data could not be saved.");
// On retry, it should succeed
ajaxSuccess = true;
requestEmailChange("bob@example.com", "password");
assertRequestStatus(true, "Please check your email to confirm the change");
assertStatus(view.$requestStatus, true, "error", "Please check your email to confirm the change");
});
it("requests a password reset", function() {
requestPasswordChange();
assertAjax("password", "POST", {});
assertStatus(view.$passwordResetStatus, true, "error", "Password reset email sent. Follow the link in the email to change your password.");
});
it("displays an error message if a password reset email could not be sent", function() {
// Simulate an error from the server
ajaxSuccess = false;
requestPasswordChange();
assertStatus(view.$passwordResetStatus, false, "error", "We weren't able to send you a password reset email.");
// Retry, this time simulating success
ajaxSuccess = true;
requestPasswordChange();
assertStatus(view.$passwordResetStatus, true, "error", "Password reset email sent. Follow the link in the email to change your password.");
});
});
}

View File

@@ -71,11 +71,12 @@ var edx = edx || {};
events: {
'submit': 'submit',
'change': 'change'
'change': 'change',
'click #password-reset': 'click'
},
initialize: function() {
_.bindAll(this, 'render', 'submit', 'change', 'clearStatus', 'invalid', 'error', 'sync');
_.bindAll(this, 'render', 'submit', 'change', 'click', 'clearStatus', 'invalid', 'error', 'sync');
this.model = new edx.student.account.AccountModel();
this.model.on('invalid', this.invalid);
this.model.on('error', this.error);
@@ -89,6 +90,9 @@ var edx = edx || {};
this.$emailStatus = $('#new-email-status', this.$el);
this.$passwordStatus = $('#password-status', this.$el);
this.$requestStatus = $('#request-email-status', this.$el);
this.$passwordReset = $('#password-reset', this.$el);
this.$passwordResetStatus = $('#password-reset-status', this.$el);
return this;
},
@@ -105,6 +109,31 @@ var edx = edx || {};
});
},
click: function(event) {
event.preventDefault();
this.clearStatus();
self = this;
$.ajax({
url: 'password',
type: 'POST',
data: {},
headers: {
'X-CSRFToken': $.cookie('csrftoken')
}
})
.done(function() {
self.$passwordResetStatus
.addClass('success')
.text(gettext("Password reset email sent. Follow the link in the email to change your password."));
})
.fail(function() {
self.$passwordResetStatus
.addClass('error')
.text(gettext("We weren't able to send you a password reset email."));
});
},
invalid: function(model) {
var errors = model.validationError;
@@ -145,6 +174,10 @@ var edx = edx || {};
this.$requestStatus
.removeClass('error')
.text("");
this.$passwordResetStatus
.removeClass('error')
.text("");
},
});

View File

@@ -262,3 +262,19 @@ footer .references {
.dashboard {
padding-top: 60px;
}
// ====================
// poor definition/scope on ul elements inside .vert-mod element in courseware - override needed for inline discussion editing
.course-content .discussion-post.edit-post-form .topic-menu {
padding-left: 0;
list-style: none;
.topic-menu-item {
margin-bottom: 0;
}
}
.course-content .discussion-post.edit-post-form .topic-submenu {
list-style: none;
}

View File

@@ -0,0 +1,61 @@
## NOTE: This Sass infrastructure is redundant, but needed in order to address an IE9 rule limit within CSS - http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx
// lms - css application architecture (platform)
// ====================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'base/reset';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env["FEATURES"].get("USE_CUSTOM_THEME", False):
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}';
% endif
@import 'base/base';
// base - assets
@import 'base/font_face';
@import 'base/extends';
@import 'base/animations';
// base - starter
@import 'base/base';
// base - elements
@import 'elements/typography';
@import 'elements/controls';
// shared - platform
@import 'multicourse/home';
@import 'multicourse/dashboard';
@import 'multicourse/account';
@import 'multicourse/courses';
@import 'multicourse/course_about';
@import 'multicourse/jobs';
@import 'multicourse/media-kit';
@import 'multicourse/about_pages';
@import 'multicourse/press_release';
@import 'multicourse/error-pages';
@import 'multicourse/help';
@import 'multicourse/edge';
@import 'developer'; // used for any developer-created scss that needs further polish/refactoring
@import 'shame'; // used for any bad-form/orphaned scss
## NOTE: needed here for cascade and dependency purposes, but not a great permanent solution

View File

@@ -1,11 +1,11 @@
## NOTE: This Sass infrastructure is redundant, but needed in order to address an IE9 rule limit within CSS - http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx
// lms - css application architecture (platform)
// ====================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
// BASE *default edX offerings*
// ====================

View File

@@ -0,0 +1,69 @@
## NOTE: This Sass infrastructure is redundant, but needed in order to address an IE9 rule limit within CSS - http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx
// lms - css application architecture (platform)
// ====================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'base/reset';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env["FEATURES"].get("USE_CUSTOM_THEME", False):
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}';
% endif
@import 'base/base';
// base - assets
@import 'base/font_face';
@import 'base/extends';
@import 'base/animations';
// base - starter
@import 'base/base';
// base - elements
@import 'elements/typography';
@import 'elements/controls';
@import 'elements/system-feedback';
// base - specific views
@import 'views/verification';
@import 'views/shoppingcart';
// applications
@import "discussion/utilities/variables";
@import "discussion/mixins";
@import 'discussion/discussion'; // Process old file after definitions but before everything else
@import "discussion/elements/actions";
@import "discussion/elements/editor";
@import "discussion/elements/labels";
@import "discussion/elements/navigation";
@import "discussion/views/thread";
@import "discussion/views/create-edit-post";
@import "discussion/views/response";
@import 'discussion/utilities/developer';
@import 'discussion/utilities/shame';
@import 'news';
// temp - shame and developer
@import 'developer'; // used for any developer-created scss that needs further polish/refactoring
@import 'shame'; // used for any bad-form/orphaned scss
## NOTE: needed here for cascade and dependency purposes, but not a great permanent solution

View File

@@ -1,11 +1,11 @@
## NOTE: This Sass infrastructure is redundant, but needed in order to address an IE9 rule limit within CSS - http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx
// lms - css application architecture (platform)
// ====================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
// BASE *default edX offerings*
// ====================

View File

@@ -0,0 +1,57 @@
## Note: This Sass infrastructure is repeated in application-extend1 and application-extend2, but needed in order to address an IE9 rule limit within CSS - http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx
// lms - css application architecture
// ====================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'base/reset';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env["FEATURES"].get("USE_CUSTOM_THEME", False):
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}';
% endif
@import 'base/base';
// base - assets
@import 'base/font_face';
@import 'base/extends';
@import 'base/animations';
// base - starter
@import 'base/base';
// base - elements
@import 'elements/typography';
@import 'elements/controls';
// shared - course
@import 'shared/forms';
@import 'shared/footer';
@import 'shared/header';
@import 'shared/course_object';
@import 'shared/course_filter';
@import 'shared/modal';
@import 'shared/activation_messages';
@import 'shared/unsubscribe';
@import 'developer'; // used for any developer-created scss that needs further polish/refactoring
@import 'shame'; // used for any bad-form/orphaned scss
## NOTE: needed here for cascade and dependency purposes, but not a great permanent solution

View File

@@ -5,6 +5,7 @@
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
// BASE *default edX offerings*
// ====================

View File

@@ -107,6 +107,7 @@ a:focus {
.container {
@include clearfix;
@include box-sizing(border-box);
margin: 0 auto 0;
padding: 0px 30px;
max-width: grid-width(12);

View File

@@ -54,7 +54,6 @@
// ====================
// extends - UI - used for page/view-level wrappers (for centering/grids)
%ui-wrapper {
@include clearfix();

View File

@@ -1,3 +1,5 @@
// lms variables
// base
$baseline: 20px;

View File

@@ -0,0 +1,77 @@
@import 'bourbon/bourbon';
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
@import 'base/reset';
@import 'base/font_face';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env["FEATURES"].get("USE_CUSTOM_THEME", False):
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}';
% endif
@import 'base/base';
@import 'base/extends';
@import 'base/animations';
@import 'shared/tooltips';
// base - elements
@import 'elements/typography';
@import 'elements/controls';
// Course base / layout styles
@import 'course/layout/courseware_header';
@import 'course/layout/footer';
@import 'course/base/mixins';
@import 'course/base/base';
@import 'course/base/extends';
@import 'xmodule/modules/css/module-styles.scss';
// courseware
@import 'course/courseware/courseware';
@import 'course/courseware/sidebar';
@import 'course/courseware/amplifier';
@import 'course/layout/calculator';
@import 'course/layout/timer';
@import 'course/layout/chat';
// course-specific courseware (all styles in these files should be gated by a
// course-specific class). This should be replaced with a better way of
// providing course-specific styling.
@import "course/courseware/courses/_cs188.scss";
// wiki
@import "course/wiki/basic-html";
@import "course/wiki/sidebar";
@import "course/wiki/create";
@import "course/wiki/wiki";
@import "course/wiki/table";
// pages
@import "course/info";
@import "course/syllabus"; // TODO arjun replace w/ custom tabs, see courseware/courses.py
@import "course/textbook";
@import "course/profile";
@import "course/gradebook";
@import "course/tabs";
@import "course/staff_grading";
@import "course/rubric";
@import "course/open_ended_grading";
// instructor
@import "course/instructor/instructor";
@import "course/instructor/instructor_2";
@import "course/instructor/email";
@import "xmodule/descriptors/css/module-styles.scss";
// discussion
@import "course/discussion/form-wmd-toolbar";

View File

@@ -1,4 +1,5 @@
@import 'bourbon/bourbon';
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
@import 'base/reset';
@import 'base/font_face';

View File

@@ -80,7 +80,7 @@ div.info-wrapper {
section.handouts {
@extend .sidebar;
border-radius: 0 4px 4px 0;
border-left: 1px solid #ddd;
@include border-left(1px solid #ddd);
box-shadow: none;
font-size: 14px;

View File

@@ -141,7 +141,7 @@
margin-bottom: lh();
h1 {
float: left;
@include float(left);
font-size: 1em;
font-weight: 100;
margin: 0;
@@ -188,7 +188,7 @@
}
h2 {
border-right: 1px dashed #ddd;
@include border-right(1px dashed #ddd);
@include box-sizing(border-box);
display: table-cell;
letter-spacing: 0;
@@ -201,7 +201,7 @@
.sections {
display: table-cell;
padding-left: flex-gutter(9);
@include padding-left(flex-gutter(9));
width: flex-grid(7, 9);
> div {

View File

@@ -6,7 +6,7 @@ body {
}
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a, label {
text-align: left;
@include text-align(left);
font-family: $sans-serif;
}
@@ -49,7 +49,7 @@ form {
form.choicegroup {
label {
clear: both;
float: left;
@include float(left);
}
}

View File

@@ -1,6 +1,6 @@
h1.top-header {
border-bottom: 1px solid $border-color-2;
text-align: left;
@include text-align(left);
font-size: em(24);
font-weight: 100;
padding-bottom: lh();

View File

@@ -1,3 +1,4 @@
@mixin blue-button {
display: block;
height: 35px;
@@ -54,4 +55,4 @@
&:hover, &:focus {
background: -webkit-linear-gradient(top, #888, #666);
}
}
}

View File

@@ -23,7 +23,7 @@ html.video-fullscreen{
.instructor-info-action {
@extend %t-copy-sub2;
float: right;
@include float(right);
margin-left: ($baseline/2);
padding: ($baseline/4) ($baseline/2);
border-radius: ($baseline/4);

View File

@@ -1,8 +1,8 @@
.course-index {
@extend .sidebar;
@extend .tran;
border-radius: 3px 0 0 3px;
border-right: 1px solid $border-color-2;
@include border-right(1px solid $border-color-2);
@include border-radius(3px, 0, 0, 3px);
#open_close_accordion {
display: none;
@@ -47,7 +47,7 @@
a {
border-radius: 0;
box-shadow: none;
padding-left: 19px;
@include padding-left(19px);
color: $link-color;
}
@@ -61,9 +61,22 @@
}
span.ui-icon {
left: 0;
background-image: url("/static/images/ui-icons_222222_256x240.png");
@include left(0);
opacity: 0.3;
background-image: url("/static/images/ui-icons_222222_256x240.png"); // jQuery UI sprite
&.ui-icon-triangle-1-e {
// CASE: left to right layout
@include ltr {
background-position: -32px -16px; // jQuery UI east arrow position
}
// CASE: right to left layout
@include rtl {
background-position: -96px -16px; // jQuery UI west arrow position
}
}
}
}
}

View File

@@ -185,3 +185,8 @@
}
.rtl .instructor-dashboard-wrapper .beta-button-wrapper,
.rtl .instructor-dashboard-wrapper .studio-edit-link {
left: 2em;
right: auto;
}

View File

@@ -785,7 +785,6 @@ section.instructor-dashboard-content-2 {
.info {
@include box-sizing(border-box);
padding: ($baseline/2);
border: 1px solid $light-gray;
color: $lighter-base-font-color;
line-height: 1.3em;
@@ -833,7 +832,7 @@ section.instructor-dashboard-content-2 {
input[type="button"].add {
@include idashbutton($blue);
position: absolute;
right: $baseline;
@include right($baseline);
}
}
@@ -1621,3 +1620,8 @@ input[name="subject"] {
}
}
.rtl .instructor-dashboard-wrapper-2 .olddash-button-wrapper,
.rtl .instructor-dashboard-wrapper-2 .studio-edit-link {
left: 2em;
right: auto;
}

View File

@@ -16,10 +16,10 @@ nav.course-material {
@include border-top-radius(4px);
@include clearfix;
padding: 28px 0 10px 0;
margin-left: 10px;
@include margin-left(10px);
li {
float: left;
@include float(left);
list-style: none;
margin-right: 6px;
@@ -120,7 +120,7 @@ header.global.slim {
h1.logo {
margin: 0 10px 0 13px;
padding-right: 20px;
@include padding-right(20px);
&:before {
@extend %faded-vertical-divider;
@@ -128,7 +128,7 @@ header.global.slim {
display: block;
height: 35px;
position: absolute;
right: 3px;
@include right(3px);
top: 0;
width: 1px;
}
@@ -156,7 +156,7 @@ header.global.slim {
h2 {
display: block;
width: 700px;
float: left;
@include float(left);
font-size: 0.9em;
font-weight: 600;
color: $lighter-base-font-color;

View File

@@ -18,7 +18,7 @@ nav.course-material {
padding: 10px 0 0 0;
li {
float: left;
@include float(left);
list-style: none;
a {

View File

@@ -3,15 +3,16 @@
body.discussion {
.course-tabs .right {
float: right;
@include float(right);
.new-post-btn {
@include blue-button;
margin-right: 4px;
@include margin-right(4px);
}
.new-post-icon {
margin: 8px 7px 0 0;
margin-top: 8px;
@include margin-right(7px);
font-size: 16px;
vertical-align: middle;
color: $white;
@@ -241,7 +242,7 @@ body.discussion {
}
.discussion-column {
float: right;
@include float(right);
@include box-sizing(border-box);
width: 68%;
max-width: 800px;
@@ -368,7 +369,7 @@ body.discussion {
.notification-checkbox {
display: inline-block;
padding: $baseline/4 0 $baseline/2 0;
margin-right: $baseline/2;
@include margin-right($baseline/2);
border-radius: 5px;
border: 1px solid gray;
@@ -376,7 +377,7 @@ body.discussion {
display: inline-block;
text-align: center;
vertical-align: middle;
margin-left: $baseline/2;
@include margin-left($baseline/2);
}
.icon {
@@ -531,6 +532,13 @@ body.discussion {
border-radius: 3px 3px 0 0;
padding: $baseline;
background-color: $white;
.response-body {
ol, ul { // Fix up the RTL-only _reset.scss, but only in specific places
@include padding-left(40px);
@include padding-right(0);
}
}
}
.posted-by {
font-weight: 700;
@@ -549,11 +557,21 @@ body.discussion {
padding: 0 18px;
width: 100%;
box-shadow: 0 1px 1px $shadow-l1;
text-align: left;
@include text-align(left);
font-size: 13px;
.icon-reply:before { // flip the icon for RTL
@include ltr {
content: "\f112"; // FA icon arrow to the left
}
@include rtl {
content: "\f064"; // FA icon arrow to the right
}
}
span.add-response-btn-text {
padding-left: ($baseline/5);
@include padding-left($baseline/5);
}
}
}
@@ -638,7 +656,7 @@ body.discussion {
.discussion-submit-post {
@include blue-button;
float: left;
@include float(left);
}
.wmd-button {
@@ -674,7 +692,17 @@ body.discussion {
padding-left: ($baseline*1.5);
width: 100%;
box-shadow: 0 1px 1px $shadow-l1;
text-align: left;
@include text-align(left);
.icon-reply:before {
@include ltr {
content: "\f112"; // FA icon arrow to the left
}
@include rtl {
content: "\f064"; // FA icon arrow to the right
}
}
&:hover, &:focus {
@include linear-gradient(top, $white 35%, #ddd);
@@ -706,7 +734,7 @@ body.discussion {
display: inline-block;
position: relative;
top: 5px;
margin-right: 6px;
@include margin-right(6px);
width: 21px;
height: 19px;
background: url(../images/show-hide-discussion-icon.png) no-repeat;
@@ -716,7 +744,7 @@ body.discussion {
.new-post-btn {
display: inline-block;
float: right;
@include float(right);
}
section.discussion {
@@ -895,9 +923,10 @@ body.discussion {
float: left;
width: 16px;
height: 17px;
margin: 8px 7px 0 0;
margin-top: 8px;
@include margin-right(7px);
font-size: 16px;
padding-right: $baseline/2;
@include padding-right($baseline/2);
vertical-align: middle;
color: $white;
}

View File

@@ -89,7 +89,7 @@
@mixin discussion-wmd-preview-label {
padding-top: 3px;
padding-left: 5px;
@include padding-left(5px);
width: 100%;
color: #bbb;
text-transform: uppercase;
@@ -100,6 +100,11 @@
padding: 10px 20px;
width: 100%;
color: #333;
ol, ul { // Fix up the RTL-only _reset.scss, but only in specific places
@include padding-left(40px);
@include padding-right(0);
}
}
@-webkit-keyframes fadeIn {
@@ -130,11 +135,11 @@
color: $color;
.icon {
margin-right: ($baseline/5);
@include margin-right($baseline/5);
}
&:last-child {
margin-right: 0;
@include margin-right(0);
}
&.is-hidden {

View File

@@ -8,7 +8,7 @@
.response-actions-list,
.comment-actions-list {
@extend %ui-no-list;
text-align: right;
@include text-align(right);
.actions-item {
@include box-sizing(border-box);
@@ -58,7 +58,7 @@
&:after,
&:before {
bottom: 100%;
right: 3px;
@include right(3px);
border: solid transparent;
content: " ";
height: 0;
@@ -71,7 +71,7 @@
border-color: $transparent;
border-bottom-color: $white;
border-width: 6px;
margin-right: 1px;
@include margin-right(1px);
}
&:before {
@@ -254,7 +254,7 @@
display: block;
padding: ($baseline/10) 0;
white-space: nowrap;
text-align: right;
@include text-align(right);
color: $gray-l1;
&:hover, &:focus {

View File

@@ -3,7 +3,7 @@
.forum-nav {
@include box-sizing(border-box);
float: left;
@include float(left);
position: relative;
width: 31%;
border: 1px solid #aaa;
@@ -34,7 +34,7 @@
.icon {
@include font-size(14);
margin-right: ($baseline/4);
@include margin-right($baseline/4);
}
}
@@ -43,7 +43,7 @@
}
.forum-nav-browse-drop-arrow {
margin-left: ($baseline/4);
@include margin-left($baseline/4);
}
.forum-nav-search {
@@ -60,7 +60,7 @@
position: absolute;
margin-top: -6px;
top: 50%;
right: ($baseline/4 + 1px + $baseline / 4); // Wrapper padding + border + input padding
@include right($baseline/4 + 1px + $baseline / 4); // Wrapper padding + border + input padding
}
.forum-nav-search-input {
@@ -115,7 +115,7 @@
}
.forum-nav-browse-title .icon {
margin-right: ($baseline/2);
@include margin-right($baseline/2);
}
// -------------------
@@ -135,14 +135,14 @@
@include box-sizing(border-box);
display: inline-block;
width: 50%;
text-align: left;
@include text-align(left);
}
.forum-nav-filter-cohort, .forum-nav-sort {
@include box-sizing(border-box);
display: inline-block;
width: 50%;
text-align: right;
@include text-align(right);
}
%forum-nav-select {
@@ -200,6 +200,13 @@
.icon {
@include font-size(14);
&:before {
@include rtl {
@include transform(scale(-1, 1)); // RTL for font awesome question mark
}
}
}
.icon-comments {
@@ -223,7 +230,7 @@
.forum-nav-thread-wrapper-2 {
@extend %forum-nav-thread-wrapper;
width: 13%;
text-align: right;
@include text-align(right);
}
.forum-nav-thread-title {
@@ -251,7 +258,7 @@
@extend %forum-nav-thread-wrapper-2-content;
@extend %t-weight4;
position: relative;
margin-left: ($baseline/4);
@include margin-left($baseline/4);
margin-bottom: ($baseline/4); // Because tail is position: absolute
border-radius: 2px;
padding: ($baseline/10) ($baseline/5);
@@ -264,12 +271,12 @@
display: block;
position: absolute;
bottom: (-$baseline/4);
right: ($baseline/4);
@include right($baseline/4);
width: 0;
height: 0;
border-style: solid;
border-width: 0 ($baseline/4) ($baseline/4) 0;
border-color: transparent $gray-l3 transparent transparent;
@include border-width(0, ($baseline/4), ($baseline/4), 0);
@include border-color(transparent, $gray-l3, transparent, transparent);
}
&.is-unread {

View File

@@ -46,7 +46,7 @@
.field-help {
@include box-sizing(border-box);
display: inline-block;
padding-left: $baseline;
@include padding-left($baseline);
width: 50%;
font-size: 12px;
}
@@ -131,7 +131,7 @@
.post-option {
@include box-sizing(border-box);
display: inline-block;
margin-right: $baseline;
@include margin-right($baseline);
border: 1px solid transparent;
border-radius: 3px;
padding: ($baseline/2);

View File

@@ -17,7 +17,7 @@ body.discussion, .discussion-module {
.post-header-actions {
display: inline-block;
float: right;
@include float(right);
vertical-align: middle;
width: flex-grid(3,12);
}
@@ -44,7 +44,7 @@ body.discussion, .discussion-module {
.response-header-actions {
width: flex-grid(3,12);
float: right;
@include float(right);
}
}

View File

@@ -123,16 +123,16 @@
// CASE: normal typographical headings
h1 {
@extend %heading-2;
@include text-align(left);
margin-bottom: $baseline;
padding-bottom: $baseline;
text-align: left;
}
// CASE: marketing/imageery-based headings
.title {
position: absolute;
top: ($baseline*2.5);
left:($baseline*1.5);
@include left($baseline*1.5);
.title-super, .title-sub {
@extend %t-weight1;
@@ -149,7 +149,7 @@
.title-sub {
@include font-size(20);
margin-left: ($baseline*2);
@include margin-left($baseline*2);
text-transform: lowercase;
color: $header-graphic-sub-color;
}
@@ -197,14 +197,14 @@
}
.content {
margin-right: ($baseline*2);
@include float(left);
@include margin-right($baseline*2);
width: 600px;
float: left;
}
aside {
@include float(left);
width: 280px;
float: left;
p, ol, ul {
font-size: 14px !important;
@@ -273,10 +273,10 @@
margin: 0 0 $baseline 0;
.field {
@include float(left);
@include margin(0, ($baseline*1.5), 0, 0);
display: block;
float: left;
border-bottom: none;
margin: 0 ($baseline*1.5) 0 0;
padding-bottom: 0;
input, textarea {
@@ -336,7 +336,7 @@
.tip {
position: absolute;
top: 0;
right: 0;
@include right(0);
}
}
@@ -378,7 +378,7 @@
input[type="checkbox"] {
display: inline-block;
width: auto;
margin-right: ($baseline/4);
@include margin-right($baseline/4);
}
label {
@@ -702,7 +702,7 @@
h2 {
@extend %heading-2;
text-align: left;
@include text-align(left);
}
}

View File

@@ -295,7 +295,7 @@
}
.details {
float: left;
@include float(left);
margin-right: flex-gutter();
width: flex-grid(8);
font: normal 1em/1.6em $serif;
@@ -384,7 +384,7 @@
.course-sidebar {
@include box-sizing(border-box);
float: left;
@include float(left);
width: flex-grid(4);
> section {

View File

@@ -42,14 +42,14 @@
.logo {
display: inline-block;
@include border-right(1px solid $light-gray);
height: 80px;
margin-right: 30px;
padding-right: 30px;
@include margin-right(30px);
@include padding-right(30px);
position: relative;
vertical-align: middle;
&::after {
@extend %faded-vertical-divider;
content: "";
display: block;
height: 80px;

View File

@@ -6,8 +6,9 @@
padding: ($baseline*2) 0 0 0;
.profile-sidebar {
float: left;
margin-right: flex-gutter();
background: transparent;
@include float(left);
@include margin-right(flex-gutter());
width: flex-grid(3);
background: transparent;
box-shadow: 0 0 1px $shadow-l1;
@@ -278,7 +279,7 @@
// course listings
.my-courses {
float: left;
@include float(left);
margin: 0px;
width: flex-grid(9);
@@ -355,7 +356,7 @@
@include transition(all 0.15s linear 0s);
overflow: hidden;
position: relative;
float: left;
@include float(left);
height: 100%;
max-height: 100%;
width: 200px;
@@ -374,7 +375,7 @@
.info {
@include clearfix;
padding: 0 10px 0 230px;
@include padding(0, 10px, 0, 230px);
> hgroup {
padding: 0;
@@ -393,7 +394,7 @@
.date-block {
position: absolute;
top: 0;
right: 0;
@include right(0);
font-family: $sans-serif;
font-size: 13px;
font-style: italic;
@@ -451,7 +452,7 @@
@include box-sizing(border-box);
border-radius: 3px;
display: block;
float: left;
@include float(left);
font: normal 15px/1.6rem $sans-serif;
letter-spacing: 0;
padding: 6px 32px 7px;
@@ -888,7 +889,7 @@
}
a.unenroll {
float: right;
@include float(right);
display: block;
font-style: italic;
color: $lighter-base-font-color;
@@ -903,7 +904,7 @@
a.email-settings {
@extend a.unenroll;
margin-right: 10px;
@include margin-right(10px);
}
}

View File

@@ -1,13 +1,13 @@
section.outside-app {
@extend .container;
text-align: left;
@include text-align(left);
padding: 80px 0;
h1 {
@extend h2;
margin-bottom: 40px;
}
p {
max-width: 600px;
margin: 0 auto;

View File

@@ -5,11 +5,11 @@
.university-column {
width: flex-grid(4);
margin-right: flex-gutter();
@include margin-right(flex-gutter());
float: left;
&:nth-child(3n+3) {
margin-right: 0;
@include margin-right(0);
}
}
@@ -21,11 +21,11 @@
.courses-listing-item {
width: flex-grid(4);
margin-right: flex-gutter();
float: left;
@include margin-right(flex-gutter());
@include float(left);
&:nth-child(3n+3) {
margin-right: 0;
@include margin-right(0);
}
}
}

View File

@@ -8,6 +8,7 @@
footer {
@include clearfix();
@include box-sizing(border-box);
max-width: grid-width(12);
min-width: 760px;
width: flex-grid(12);
@@ -32,16 +33,16 @@
// colophon
.colophon {
margin-right: flex-gutter();
@include margin-right(flex-gutter());
width: flex-grid(8,12);
float: left;
@include float(left);
.nav-colophon {
@include clearfix();
margin: $footer_margin;
li {
float: left;
@include float(left);
margin-right: ($baseline*0.75);
a {
@@ -102,12 +103,12 @@
margin: -2px 0 8px 0;
font-size: em(11);
color: $gray-l2;
text-align: left;
@include text-align(left);
}
.nav-legal {
@include clearfix();
text-align: left;
@include text-align(left);
li {
display: inline-block;
@@ -154,10 +155,11 @@
// platform Open edX logo and link
.powered-by {
@include float(right);
width: flex-grid(3,12);
display: inline-block;
vertical-align: bottom;
text-align: right;
@include text-align(right);
a {
display: inline-block;

View File

@@ -17,8 +17,8 @@ header.global {
}
h1.logo {
float: left;
margin: -2px 39px 0px 0px;
@include float(left);
@include margin(-2px, 39px, 0, 0);
position: relative;
a {
@@ -27,11 +27,11 @@ header.global {
}
.left {
float: left;
@include float(left);
}
.guest {
float: right;
@include float(right);
}
> li {
@@ -104,12 +104,12 @@ header.global {
}
.user {
float: right;
@include float(right);
margin-top: 4px;
> .primary {
display: block;
float: left;
@include float(left);
margin: 0px;
position: relative;
@@ -120,14 +120,14 @@ header.global {
&:last-child {
> a {
border-radius: 0 4px 4px 0;
border-left: none;
@include border-radius(0, 4px, 4px, 0);
@include border-left(none);
padding: 5px 8px 7px 8px;
&.shopping-cart {
border-radius: 4px;
border: 1px solid $border-color-2;
margin-right: 10px;
@include margin-right(10px);
padding-bottom: 6px;
}
}
@@ -135,7 +135,7 @@ header.global {
}
a.user-link {
padding: 6px 12px 8px 35px;
@include padding(6px, 12px, 8px, 35px);
position: relative;
text-transform: none;
font-size: 14px;
@@ -145,9 +145,15 @@ header.global {
.avatar {
@include background-image(url('../images/small-header-home-icon.png'));
background-repeat: no-repeat;
// CASE: right to left layout
@include rtl {
background-position: top right;
}
height: 26px;
display: inline-block;
left: 8px;
@include left(8px);
opacity: 0.5;
overflow: hidden;
position: absolute;
@@ -171,7 +177,7 @@ header.global {
display: none;
padding: 5px 10px;
position: absolute;
right: 0px;
@include right(0px);
top: 34px;
width: 170px;
z-index: 3;
@@ -194,7 +200,7 @@ header.global {
height: 0px;
position: absolute;
@include transform(rotate(-45deg));
right: 12px;
@include right(12px);
top: -6px;
width: 0px;
}
@@ -236,7 +242,7 @@ header.global {
.nav-global {
margin-top: ($baseline/2);
list-style: none;
float: left;
@include float(left);
li,
div {
@@ -279,7 +285,7 @@ header.global {
}
.nav-courseware {
float: right;
@include float(right);
margin-top: ($baseline/4);
list-style: none;
@@ -334,8 +340,7 @@ header.global-new {
nav {
@include clearfix();
max-width: grid-width(12);
min-width: 760px;
width: grid-width(12);
height: ($baseline*2);
margin: 0 auto;
padding: 18px ($baseline/2) 0;
@@ -395,7 +400,7 @@ header.global-new {
}
.primary {
margin-right: 5px;
@include margin-right(5px);
> a {
@include background-image($button-bg-image);
@@ -416,7 +421,7 @@ header.global-new {
vertical-align: middle;
&:last-child {
margin-right: 0px;
@include margin-right(0);
}
&:hover, &:focus, &:active {
@@ -431,13 +436,13 @@ header.global-new {
> .primary {
display: block;
float: left;
margin: 0px;
@include float(left);
margin: 0;
position: relative;
> a {
margin: 0px;
@include border-right-radius(0px);
@include border-right-radius(0);
}
&:last-child {
@@ -457,7 +462,7 @@ header.global-new {
}
a.user-link {
padding: 6px 12px 8px 35px;
@include padding(6px, 12px, 8px, 35px);
position: relative;
text-transform: none;
font-size: 14px;
@@ -469,7 +474,7 @@ header.global-new {
background-repeat: no-repeat;
height: 26px;
display: inline-block;
left: 8px;
@include left(8px);
opacity: 0.5;
overflow: hidden;
position: absolute;
@@ -493,7 +498,7 @@ header.global-new {
display: none;
padding: 5px 10px;
position: absolute;
right: 0px;
@include right(0);
top: 34px;
width: 170px;
z-index: 3;
@@ -516,7 +521,7 @@ header.global-new {
height: 0px;
position: absolute;
@include transform(rotate(-45deg));
right: 12px;
@include right(12px);
top: -6px;
width: 0px;
}

View File

@@ -0,0 +1,11 @@
// ------------------------------------------
// left to right module
// authors:
// twitter.com/anasnakawa
// twitter.com/victorzamfir
// licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.php
// ------------------------------------------
@import 'variables-ltr';
@import 'mixins';

View File

@@ -0,0 +1,11 @@
// ------------------------------------------
// right to left module
// authors:
// twitter.com/anasnakawa
// twitter.com/victorzamfir
// licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.php
// ------------------------------------------
@import 'variables-rtl';
@import 'mixins';

294
lms/static/sass/vendor/bi-app/_mixins.scss vendored Executable file
View File

@@ -0,0 +1,294 @@
// ------------------------------------------
// bi app mixins
// authors:
// twitter.com/anasnakawa
// twitter.com/victorzamfir
// licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.php
// ------------------------------------------
// ------------------------------------------
// Table of contents
// ------------------------------------------
// padding
// margin
// float
// text align
// clear
// left / right
// border
// - width
// - style
// - color
// - generic
// - radius
// ltr / rtl contents
// ------------------------------------------
// generic mixin for properties with values
// (top right bottom left)
// ------------------------------------------
@mixin bi-app-compact($property, $top, $right, $bottom, $left) {
@if $bi-app-direction == ltr {
#{$property}: $top $right $bottom $left;
} @else {
#{$property}: $top $left $bottom $right;
}
}
// padding
// ------------------------------------------
@mixin padding-left($distance) {
padding-#{$bi-app-left}: $distance;
}
@mixin padding-right($distance) {
padding-#{$bi-app-right}: $distance;
}
@mixin padding($top, $right, $bottom, $left) {
@include bi-app-compact(padding, $top, $right, $bottom, $left);
}
// margin
// ------------------------------------------
@mixin margin-left($distance) {
margin-#{$bi-app-left}: $distance;
}
@mixin margin-right($distance) {
margin-#{$bi-app-right}: $distance;
}
@mixin margin($top, $right, $bottom, $left) {
@include bi-app-compact(margin, $top, $right, $bottom, $left);
}
// float
// ------------------------------------------
@mixin bi-app-float-left {
float: $bi-app-left;
}
@mixin bi-app-float-right {
float: $bi-app-right;
}
@mixin float($direction) {
@if $direction == left {
@include bi-app-float-left;
} @else if $direction == right {
@include bi-app-float-right;
} @else {
float: $direction;
}
}
// text align
// ------------------------------------------
@mixin bi-app-text-align-left {
text-align: $bi-app-left;
}
@mixin bi-app-text-align-right {
text-align: $bi-app-right;
}
@mixin text-align($direction) {
@if $direction == left {
@include bi-app-text-align-left;
} @else if $direction == right {
@include bi-app-text-align-right;
} @else {
text-align: $direction;
}
}
// clear
// ------------------------------------------
@mixin bi-app-clear-left {
clear: $bi-app-left;
}
@mixin bi-app-clear-right {
clear: $bi-app-right;
}
@mixin clear($direction) {
@if $direction == left {
@include bi-app-clear-left;
} @else if $direction == right {
@include bi-app-clear-right;
} @else {
clear: $direction;
}
}
// left / right
// ------------------------------------------
@mixin left($distance) {
@if $bi-app-direction == ltr {
left: $distance;
} @else if $bi-app-direction == rtl {
right: $distance;
}
}
@mixin right($distance) {
@if $bi-app-direction == ltr {
right: $distance;
} @else if $bi-app-direction == rtl {
left: $distance;
}
}
// border
// ------------------------------------------
// width
@mixin border-left-width($width) {
border-#{$bi-app-left}-width: $width;
}
@mixin border-right-width($width) {
border-#{$bi-app-right}-width: $width;
}
@mixin border-width($top, $right, $bottom, $left) {
@include bi-app-compact(border-width, $top, $right, $bottom, $left);
}
// style
@mixin border-left-style($style) {
border-#{$bi-app-left}-style: $style;
}
@mixin border-right-style($style) {
border-#{$bi-app-right}-style: $style;
}
@mixin border-style($top, $right, $bottom, $left) {
@include bi-app-compact(border-style, $top, $right, $bottom, $left);
}
// color
@mixin border-left-color($color) {
border-#{$bi-app-left}-color: $color;
}
@mixin border-right-color($color) {
border-#{$bi-app-right}-color: $color;
}
@mixin border-color($top, $right, $bottom, $left) {
@include bi-app-compact(border-color, $top, $right, $bottom, $left);
}
// generic
@mixin border-left($border-style) {
border-#{$bi-app-left}: $border-style;
}
@mixin border-right($border-style) {
border-#{$bi-app-right}: $border-style;
}
// radius
@mixin border-top-left-radius($radius) {
-webkit-border-top-#{$bi-app-left}-radius: $radius;
-moz-border-top#{$bi-app-left}-radius: $radius;
border-top-#{$bi-app-left}-radius: $radius;
}
@mixin border-top-right-radius($radius) {
-webkit-border-top-#{$bi-app-right}-radius: $radius;
-moz-border-top#{$bi-app-right}-radius: $radius;
border-top-#{$bi-app-right}-radius: $radius;
}
@mixin border-bottom-left-radius($radius) {
-webkit-border-bottom-#{$bi-app-left}-radius: $radius;
-moz-border-bottom#{$bi-app-left}-radius: $radius;
border-bottom-#{$bi-app-left}-radius: $radius;
}
@mixin border-bottom-right-radius($radius) {
-webkit-border-bottom-#{$bi-app-right}-radius: $radius;
-moz-border-bottom#{$bi-app-right}-radius: $radius;
border-bottom-#{$bi-app-right}-radius: $radius;
}
@mixin border-right-radius($radius) {
@include border-top-right-radius($radius);
@include border-bottom-right-radius($radius);
}
@mixin border-left-radius($radius) {
@include border-top-left-radius($radius);
@include border-bottom-left-radius($radius);
}
@mixin border-top-radius($radius) {
@include border-top-left-radius($radius);
@include border-top-right-radius($radius);
}
@mixin border-bottom-radius($radius) {
@include border-bottom-left-radius($radius);
@include border-bottom-right-radius($radius);
}
@mixin border-radius($topLeft, $topRight: null, $bottomRight: null, $bottomLeft: null) {
@if $topRight != null {
@include border-top-left-radius($topLeft);
@include border-top-right-radius($topRight);
@include border-bottom-right-radius($bottomRight);
@include border-bottom-left-radius($bottomLeft);
} @else {
-webkit-border-radius: $topLeft;
-moz-border-radius: $topLeft;
-ms-border-radius: $topLeft;
-o-border-radius: $topLeft;
border-radius: $topLeft;
}
}
// Returns "en" or "ar", useful for image suffixes.
// Usage: background-image: url(/img/header-#{lang()}.png);
@function lang() {
@if $bi-app-direction == ltr {
@return 'en';
} @else {
@return 'ar';
}
}
// Support for "direction" declaration (renders ltr/rtl).
// Useful for form elements as they swap the text-indent property and align the text accordingly.
@mixin direction {
direction: $bi-app-direction;
}
// Inverts a percentage value. Example: 97% becames 3%.
// Useful for background-position.
@function bi-app-invert-percentage($percentage) {
@if $bi-app-direction == rtl {
@return 100% - $percentage;
} @else {
@return $percentage;
}
}
// ltr / rtl contents
// ------------------------------------------
@mixin ltr {
@if $bi-app-direction == ltr {
@content;
}
}
@mixin rtl {
@if $bi-app-direction == rtl {
@content;
}
}

View File

@@ -0,0 +1,15 @@
// ------------------------------------------
// left to right variables to be used by bi-app mixins
// authors:
// twitter.com/anasnakawa
// twitter.com/victorzamfir
// licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.php
// ------------------------------------------
// namespacing variables with bi-app to
// avoid conflicting with other global variables
$bi-app-left : left;
$bi-app-right : right;
$bi-app-direction : ltr;
$bi-app-invert-direction: rtl;

View File

@@ -0,0 +1,15 @@
// ------------------------------------------
// right to left variables to be used by bi-app mixins
// authors:
// twitter.com/anasnakawa
// twitter.com/victorzamfir
// licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.php
// ------------------------------------------
// namespacing variables with bi-app to
// avoid conflicting with other global variables
$bi-app-left : right;
$bi-app-right : left;
$bi-app-direction : rtl;
$bi-app-invert-direction: ltr;

View File

@@ -355,12 +355,13 @@
// UI: page header
.page-header {
width: flex-grid(12,12);
margin: 0 0 ($baseline/2) 0;
border-bottom: ($baseline/4) solid $m-gray-l4;
margin-bottom: 0;
border-bottom: none;
.title {
@include clearfix();
width: flex-grid(12,12);
margin: 0;
.sts-course, .sts-track {
display: inline-block;
@@ -393,96 +394,51 @@
}
}
.sts-label {
@extend %t-title7;
.sts-label, .sts-course-org, .sts-course-number, .sts-course-name {
@extend %t-title5;
@extend %t-weight4;
display: block;
margin-bottom: ($baseline/2);
border-bottom: ($baseline/10) solid $m-gray-l4;
padding-bottom: ($baseline/2);
color: $m-gray-d1;
}
.sts-course {
@extend %t-title;
width: flex-grid(9,12);
@include font-size(14);
@include line-height(14);
display: inline-block;
color: $gray;
text-transform: none;
}
.sts-course-org, .sts-course-number {
@extend %t-title5;
@extend %t-weight4;
display: inline-block;
.sts-label {
margin: 0;
border: none;
padding: 0;
}
.sts-course {
width: initial;
}
.sts-course-org {
margin-right: ($baseline/4);
margin-right: 0;
}
.sts-course-name {
@include font-size(28);
@include line-height(28);
@extend %t-weight4;
display: block;
}
}
}
// CASE: page header - experiment variant A overrides
.page-header.exp-variant-A {
margin-bottom: 0;
border-bottom: none;
.title {
margin: 0;
}
.sts-label {
display: inline-block;
margin: 0;
border: none;
padding: 0;
text-transform: none;
}
.sts-course-org {
margin-right: 0;
}
.sts-label, .sts-course-org, .sts-course-number, .sts-course-name {
@extend %t-title5;
@extend %t-weight4;
@include font-size(14);
@include line-height(14);
display: inline-block;
color: $gray;
text-transform: none;
}
.wrapper-sts {
display: inline-block;
width: flex-grid(9,12);
margin-bottom: ($baseline/4);
}
.sts-course {
width: initial;
}
.title .sts-track {
display: inline-block;
.sts-track-value {
background: $verified-color-lvl3;
.wrapper-sts {
display: inline-block;
width: flex-grid(9,12);
margin-bottom: ($baseline/4);
}
&.professional-ed {
.title .sts-track {
display: inline-block;
.sts-track-value {
background-color: $professional-color-lvl1;
background: $verified-color-lvl3;
}
&.professional-ed {
.sts-track-value {
background-color: $professional-color-lvl1;
}
}
}
}
}
@@ -1150,10 +1106,6 @@
// ====================
// UI: main content
.wrapper-content-main {
}
.content-main {
width: flex-grid(12,12);
@@ -1188,20 +1140,16 @@
margin-right: flex-gutter();
&:last-child {
margin-right: 0
margin-right: 0;
}
&.help-item-technical {
width: flex-grid(8,12);
}
}
}
}
// CASE: supplemental content - experiment variant A overrides
.wrapper-content-supplementary.exp-variant-A {
.help-item-technical {
width: flex-grid(8,12);
}
}
// ====================
// VIEW: select a track
@@ -1217,7 +1165,7 @@
margin: ($baseline*2) 0;
.deco-divider {
width: flex-grid(8,12);
width: flex-grid(12,12);
float: left;
}
}
@@ -1227,7 +1175,7 @@
}
.register-choice {
width: flex-grid(8,12);
width: flex-grid(12,12);
margin: 0 flex-gutter() $baseline 0;
border-top: ($baseline/4) solid $m-gray-d4;
padding: $baseline ($baseline*1.5);
@@ -1287,7 +1235,7 @@
.list-actions {
width: flex-grid(8,8);
margin: ($baseline/3) 0;
margin: ($baseline) 0;
}
.action-select input {
@@ -1310,7 +1258,10 @@
}
.list-actions {
margin: ($baseline/3) 0;
margin: ($baseline/4) 0;
border-top: none;
width: flex-grid(4,12);
float: right;
}
.action-intro, .action-select {
@@ -1325,7 +1276,7 @@
}
.action-select {
width: flex-grid(5,8);
width: initial;
}
.action-select input {
@@ -1382,7 +1333,13 @@
.contribution-options {
@include clearfix();
margin: $baseline 0;
margin: 0;
width: flex-grid(8,12);
&:after{
clear: none;
display: none;
}
.field {
float: left;
@@ -1425,39 +1382,6 @@
}
}
// CASE: select a track - experiment variant A overrides
.wrapper-register-choose.exp-variant-A {
.register-choice {
width: flex-grid(12,12);
}
.deco-divider{
width: flex-grid(12,12);
}
.contribution-options {
width: flex-grid(8,12);
margin: 0;
&:after{
clear: none;
display: none;
}
}
.register-choice-certificate .list-actions {
border-top: none;
width: flex-grid(4,12);
float: right;
margin: ($baseline/4) 0;
.action-select {
width: initial;
}
}
}
// VIEW: requirements
&.step-requirements {

View File

@@ -31,166 +31,17 @@
<%block name="js_extra">
<%static:js group='dashboard'/>
<script type="text/javascript">
(function() {
// On initialization, set focus to the first notification available
// for screen readers.
var notifications = $('.dashboard-notifications');
if (notifications.children().length > 0) {
notifications.focus();
}
$('.message.is-expandable .wrapper-tip').bind('click', toggleExpandMessage);
function toggleExpandMessage(e) {
(e).preventDefault();
$(this).closest('.message.is-expandable').toggleClass('is-expanded');
course = $("#upgrade-to-verified").data("course-id");
analytics.track('edx.bi.dashboard.upsell_copy.clicked', {
category: 'user-engagement',
label: course
});
}
$("#failed-verification-button-dismiss").click(function(event) {
$.ajax({
url: "${reverse('verify_student_toggle_failed_banner_off')}",
type: "post"
})
$("#failed-verification-banner").addClass('is-hidden');
})
$("#upgrade-to-verified").click(function(event) {
user = $(event.target).data("user");
course = $(event.target).data("course-id");
Logger.log('edx.course.enrollment.upgrade.clicked', [user, course], null);
});
$(".email-settings").click(function(event) {
$("#email_settings_course_id").val( $(event.target).data("course-id") );
$("#email_settings_course_number").text( $(event.target).data("course-number") );
if($(event.target).data("optout") == "False") {
$("#receive_emails").prop('checked', true);
}
});
$(".unenroll").click(function(event) {
$("#unenroll_course_id").val( $(event.target).data("course-id") );
$("#unenroll_course_number").text( $(event.target).data("course-number") );
});
$('#unenroll_form').on('ajax:complete', function(event, xhr) {
if(xhr.status == 200) {
location.href = "${reverse('dashboard')}";
} else if (xhr.status == 403) {
location.href = "${reverse('signin_user')}?course_id=" +
encodeURIComponent($("#unenroll_course_id").val()) + "&enrollment_action=unenroll";
} else {
$('#unenroll_error').html(
xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}"
).stop().css("display", "block");
}
});
$('#pwd_reset_button').click(function() {
$.post('${reverse("password_reset")}',
{"email" : $('#id_email').val()},
function(data){
$("#password_reset_complete_link").click();
});
});
$("#submit-lang").click(function(event, xhr) {
event.preventDefault();
$.post('/lang_pref/setlang/',
{"language": $('#settings-language-value').val()})
.done(
function(data){
// submit form as normal
$('.settings-language-form').submit();
}
);
});
$("#change_email_form").submit(function(){
var new_email = $('#new_email_field').val();
var new_password = $('#new_email_password').val();
$.post('${reverse("change_email")}',
{"new_email" : new_email, "password" : new_password},
function(data) {
if (data.success) {
$("#change_email_title").html("${_("Please verify your new email address")}");
$("#change_email_form").html("<p>${_("You'll receive a confirmation in your inbox."
" Please follow the link in the email to confirm"
" your email address change.")}</p>");
} else {
$("#change_email_error").html(data.error).stop().css("display", "block");
}
});
return false;
});
$("#change_name_form").submit(function(){
var new_name = $('#new_name_field').val();
var rationale = $('#name_rationale_field').val();
$.post('${reverse("change_name")}',
{"new_name":new_name, "rationale":rationale},
function(data) {
if(data.success) {
location.reload();
} else {
$("#change_name_error").html(data.error).stop().css("display", "block");
}
});
return false;
});
$("#email_settings_form").submit(function(){
$.ajax({
type: "POST",
url: '${reverse("change_email_settings")}',
data: $(this).serializeArray(),
success: function(data) {
if(data.success) {
location.href = "${reverse('dashboard')}";
}
},
error: function(xhr, textStatus, error) {
if (xhr.status == 403) {
location.href = "${reverse('signin_user')}";
}
}
$(document).ready(function() {
edx.dashboard.legacy.init({
dashboard: "${reverse('dashboard')}",
signInUser: "${reverse('signin_user')}",
passwordReset: "${reverse('password_reset')}",
changeEmail: "${reverse('change_email')}",
changeEmailSettings: "${reverse('change_email_settings')}",
changeName: "${reverse('change_name')}",
verifyToggleBannerFailedOff: "${reverse('verify_student_toggle_failed_banner_off')}",
});
return false;
});
})(this);
$(function(){
accessible_modal(".edit-name", "#apply_name_change .close-modal", "#apply_name_change", "#dashboard-main");
accessible_modal(".edit-email", "#change_email .close-modal", "#change_email", "#dashboard-main");
accessible_modal("#pwd_reset_button", "#password_reset_complete .close-modal", "#password_reset_complete", "#dashboard-main");
$(".email-settings").each(function(index){
$(this).attr("id", "unenroll-" + index);
// a bit of a hack, but gets the unique selector for the modal trigger
var trigger = "#" + $(this).attr("id");
accessible_modal(trigger, "#email-settings-modal .close-modal", "#email-settings-modal", "#dashboard-main");
});
$(".unenroll").each(function(index){
$(this).attr("id", "email-settings-" + index);
// a bit of a hack, but gets the unique selector for the modal trigger
var trigger = "#" + $(this).attr("id");
accessible_modal(trigger, "#unenroll-modal .close-modal", "#unenroll-modal", "#dashboard-main");
});
});
</script>
</%block>
@@ -543,13 +394,3 @@
</form>
</div>
</section>
<script>
$(function() {
$("#unregister_block_course").click( function(event) {
$("#unenroll_course_id").val( $(event.target).data("course-id") );
$("#unenroll_course_number").text( $(event.target).data("course-number") );
});
});
</script>

View File

@@ -1,7 +1,7 @@
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<!--TODO replace this with something a clever deisgn person approves of-->
<!--TODO replace this with a shiny loopy thing to actually print out all courses-->
## TODO replace this with something a clever deisgn person approves of
## TODO replace this with a shiny loopy thing to actually print out all courses
% if reverifications["must_reverify"] or reverifications["pending"] or reverifications["denied"] or reverifications["approved"]:
<li class="status status-verification is-accepted">

View File

@@ -1,7 +1,7 @@
<div class="donation-error-msg" />
<form class="nav-item donate-form">
<span class="monetary-symbol">$</span>
<input class="amount" type="text" name="amount" value="25" />
<input class="amount" type="text" name="amount" value="5" />
<button class="btn action-primary action-donate" type="submit" name="Donate"><%- gettext('Donate') %></button>
</form>
<form class="payment-form"></form>

View File

@@ -3,11 +3,7 @@
<span class="title-value"><%- cohort.get('name') %></span>
<span class="group-count"><%-
interpolate(
ngettext(
'(contains 1 student)',
'(contains %(student_count)s students)',
cohort.get('user_count')
),
ngettext('(contains %(student_count)s student)', '(contains %(student_count)s students)', cohort.get('user_count')),
{ student_count: cohort.get('user_count') },
true
)

View File

@@ -3,7 +3,12 @@
<!--[if IE 8]><html class="ie ie8 lte9 lte8" lang="${LANGUAGE_CODE}"><![endif]-->
<!--[if IE 9]><html class="ie ie9 lte9" lang="${LANGUAGE_CODE}"><![endif]-->
<!--[if gt IE 9]><!--><html lang="${LANGUAGE_CODE}"><!--<![endif]-->
<head>
<%
# set doc language direction
from django.utils.translation import get_language_bidi
dir_rtl = 'rtl' if get_language_bidi() else 'ltr'
%>
<head dir="${dir_rtl}">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<%! from django.utils.translation import ugettext as _ %>
@@ -79,19 +84,16 @@
else:
header_extra_file = None
if settings.FEATURES.get("ENABLE_NEW_EDX_HEADER", False):
header_file = microsite.get_template_path('navigation.html')
if settings.FEATURES['IS_EDX_DOMAIN'] and not is_microsite():
header_file = microsite.get_template_path('navigation-edx.html')
else:
header_file = microsite.get_template_path('original_navigation.html')
header_file = microsite.get_template_path('navigation.html')
google_analytics_file = microsite.get_template_path('google_analytics.html')
if settings.FEATURES['IS_EDX_DOMAIN'] and not is_microsite():
if settings.FEATURES.get('ENABLE_NEW_EDX_FOOTER', False):
footer_file = microsite.get_template_path('footer-edx-new.html')
else:
footer_file = microsite.get_template_path('footer-edx.html')
footer_file = microsite.get_template_path('footer-edx-new.html')
else:
footer_file = microsite.get_template_path('footer.html')
@@ -120,20 +122,27 @@
</head>
<body class="<%block name='bodyclass'/> lang_${LANGUAGE_CODE}">
<a class="nav-skip" href="<%block name="nav_skip">#content</%block>">${_("Skip to this view's content")}</a>
<body class="${dir_rtl} <%block name='bodyclass'/> lang_${LANGUAGE_CODE}">
<div class="window-wrap" dir="${dir_rtl}">
<a class="nav-skip" href="<%block name="nav_skip">#content</%block>">${_("Skip to this view's content")}</a>
<%include file="mathjax_accessible.html" />
<%include file="mathjax_accessible.html" />
<%include file="${header_file}" />
% if not suppress_toplevel_navigation:
<%include file="${header_file}" />
%endif
<div class="content-wrapper" id="content">
${self.body()}
<%block name="bodyextra"/>
</div>
% if not suppress_toplevel_navigation:
<%include file="${footer_file}" />
% endif
<div class="content-wrapper" id="content">
${self.body()}
<%block name="bodyextra"/>
</div>
<%include file="${footer_file}" />
<script>window.baseUrl = "${settings.STATIC_URL}";</script>
% if not disable_courseware_js:
<%static:js group='application'/>

View File

@@ -32,10 +32,10 @@
<body class="{% block bodyclass %}{% endblock %} lang_{{LANGUAGE_CODE}}">
<a class="nav-skip" href="{% block nav_skip %}#content{% endblock %}">{% trans "Skip to this view's content" %}</a>
{% with course=request.course %}
{% if ENABLE_NEW_EDX_HEADER %}
{% include "navigation.html" %}
{% if IS_EDX_DOMAIN %}
{% include "navigation-edx.html" %}
{% else %}
{% include "original_navigation.html" %}
{% include "navigation.html" %}
{% endif %}
{% endwith %}
<div class="content-wrapper" id="content">
@@ -45,11 +45,7 @@
{% if IS_REQUEST_IN_MICROSITE %}
{# For now we don't support overriden Django templates in microsites. Leave footer blank for now which is better than saying Edx.#}
{% elif IS_EDX_DOMAIN %}
{% if ENABLE_NEW_EDX_FOOTER %}
{% include "footer-edx-new.html" %}
{% else %}
{% include "footer-edx.html" %}
{% endif %}
{% include "footer-edx-new.html" %}
{% else %}
{% include "footer.html" %}
{% endif %}

View File

@@ -36,30 +36,31 @@ site_status_msg = get_site_status_msg(course_id)
% endif
</%block>
<header class="global ${"slim" if course else ""}" aria-label="${_('Global Navigation')}">
<nav>
<h1 class="logo">
<a href="${marketing_link('ROOT')}">
<header class="${"global slim" if course and not disable_courseware_header else "global-new"}" aria-label="Main" role="banner">
<nav aria-label="Main">
<h1 class="logo" itemscope="" itemtype="http://schema.org/Organization">
<a href="${marketing_link('ROOT')}" title="Home page" itemprop="url">
<%block name="navigation_logo">
<img src="${static.url(branding.get_logo_url())}" alt="${platform_name()}"/>
<img src="${static.url(branding.get_logo_url())}" alt="${platform_name()}" title="${platform_name()}" itemprop="url" />
</%block>
</a>
</h1>
% if course:
% if course and not disable_courseware_header:
<h2><span class="provider">${course.display_org_with_default | h}:</span> ${course.display_number_with_default | h} ${course.display_name_with_default}</h2>
% endif
% if user.is_authenticated():
<ol class="left nav-global authenticated">
<div class="left nav-global authenticated">
<%block name="navigation_global_links_authenticated">
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
<li class="nav-global-01">
<div class="nav-global-01">
<a href="${marketing_link('COURSES')}">${_('Find Courses')}</a>
</li>
</div>
% endif
</%block>
</ol>
</div>
<ol class="user">
<li class="primary">
<a href="${reverse('dashboard')}" class="user-link">
@@ -79,6 +80,7 @@ site_status_msg = get_site_status_msg(course_id)
</ul>
</li>
</ol>
% if display_shopping_cart: # see shoppingcart.context_processor.user_has_cart_context_processor
<ol class="user">
<li class="primary">
@@ -96,37 +98,37 @@ site_status_msg = get_site_status_msg(course_id)
<a href="${marketing_link('HOW_IT_WORKS')}">${_("How it Works")}</a>
</li>
<li class="nav-global-02">
<a href="${marketing_link('COURSES')}">${_("Courses")}</a>
<a href="${marketing_link('COURSES')}">${_("Find Courses")}</a>
</li>
<li class="nav-global-03">
<a href="${marketing_link('SCHOOLS')}">${_("Schools")}</a>
<a href="${marketing_link('SCHOOLS')}">${_("Schools & Partners")}</a>
</li>
% endif
</%block>
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<li class="nav-global-04">
<a class="cta cta-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register Now")}</a>
</li>
% else:
<li class="nav-global-04">
<a class="cta cta-register" href="/register">${_("Register Now")}</a>
</li>
% endif
% endif
</ol>
<ol class="right nav-courseware">
<li class="nav-courseware-01">
<div class="right nav-courseware">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<a class="cta cta-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Log in")}</a>
% else:
<a class="cta cta-login" href="/login${login_query()}">${_("Log in")}</a>
% endif
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<div class="nav-courseware-01">
<a class="cta cta-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
</div>
% else:
<div class="nav-courseware-01">
<a class="cta cta-register" href="/register">${_("Register")}</a>
</div>
% endif
% endif
</li>
</ol>
<div class="nav-courseware-02">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<a class="cta cta-login nav-courseware-button" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Log in")}</a>
% else:
<a class="cta cta-login nav-courseware-button" href="/login${login_query()}">${_("Log in")}</a>
% endif
% endif
</div>
</div>
% endif
</nav>
</header>
@@ -140,4 +142,4 @@ site_status_msg = get_site_status_msg(course_id)
<%include file="forgot_password_modal.html" />
%endif
<%include file="help_modal.html"/>
<%include file="help_modal.html"/>

View File

@@ -36,31 +36,30 @@ site_status_msg = get_site_status_msg(course_id)
% endif
</%block>
<header class="${"global slim" if course and not disable_courseware_header else "global-new"}" aria-label="Main" role="banner">
<nav aria-label="Main">
<h1 class="logo" itemscope="" itemtype="http://schema.org/Organization">
<a href="${marketing_link('ROOT')}" title="Home page" itemprop="url">
<header class="global ${"slim" if course else ""}" aria-label="${_('Global Navigation')}">
<nav>
<h1 class="logo">
<a href="${marketing_link('ROOT')}">
<%block name="navigation_logo">
<img src="${static.url(branding.get_logo_url())}" alt="${platform_name()}" title="${platform_name()}" itemprop="url" />
<img src="${static.url(branding.get_logo_url())}" alt="${platform_name()}"/>
</%block>
</a>
</h1>
% if course and not disable_courseware_header:
% if course:
<h2><span class="provider">${course.display_org_with_default | h}:</span> ${course.display_number_with_default | h} ${course.display_name_with_default}</h2>
% endif
% if user.is_authenticated():
<div class="left nav-global authenticated">
<ol class="left nav-global authenticated">
<%block name="navigation_global_links_authenticated">
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
<div class="nav-global-01">
<li class="nav-global-01">
<a href="${marketing_link('COURSES')}">${_('Find Courses')}</a>
</div>
</li>
% endif
</%block>
</div>
</ol>
<ol class="user">
<li class="primary">
<a href="${reverse('dashboard')}" class="user-link">
@@ -80,7 +79,6 @@ site_status_msg = get_site_status_msg(course_id)
</ul>
</li>
</ol>
% if display_shopping_cart: # see shoppingcart.context_processor.user_has_cart_context_processor
<ol class="user">
<li class="primary">
@@ -98,37 +96,37 @@ site_status_msg = get_site_status_msg(course_id)
<a href="${marketing_link('HOW_IT_WORKS')}">${_("How it Works")}</a>
</li>
<li class="nav-global-02">
<a href="${marketing_link('COURSES')}">${_("Find Courses")}</a>
<a href="${marketing_link('COURSES')}">${_("Courses")}</a>
</li>
<li class="nav-global-03">
<a href="${marketing_link('SCHOOLS')}">${_("Schools & Partners")}</a>
<a href="${marketing_link('SCHOOLS')}">${_("Schools")}</a>
</li>
% endif
</%block>
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<li class="nav-global-04">
<a class="cta cta-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register Now")}</a>
</li>
% else:
<li class="nav-global-04">
<a class="cta cta-register" href="/register">${_("Register Now")}</a>
</li>
% endif
% endif
</ol>
<div class="right nav-courseware">
<ol class="right nav-courseware">
<li class="nav-courseware-01">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<div class="nav-courseware-01">
<a class="cta cta-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
</div>
% else:
<div class="nav-courseware-01">
<a class="cta cta-register" href="/register">${_("Register")}</a>
</div>
% endif
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<a class="cta cta-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Log in")}</a>
% else:
<a class="cta cta-login" href="/login${login_query()}">${_("Log in")}</a>
% endif
% endif
<div class="nav-courseware-02">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<a class="cta cta-login nav-courseware-button" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Log in")}</a>
% else:
<a class="cta cta-login nav-courseware-button" href="/login${login_query()}">${_("Log in")}</a>
% endif
% endif
</div>
</div>
</li>
</ol>
% endif
</nav>
</header>

View File

@@ -7,7 +7,7 @@
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils import html %>
<%! from django_countries.countries import COUNTRIES %>
<%! from django_countries import countries %>
<%! from student.models import UserProfile %>
<%! from datetime import date %>
<%! import calendar %>

View File

@@ -8,7 +8,7 @@
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils import html %>
<%! from django_countries.countries import COUNTRIES %>
<%! from django_countries import countries %>
<%! from django.utils.translation import ugettext as _ %>
<%! from student.models import UserProfile %>
<%! from datetime import date %>
@@ -254,7 +254,7 @@
<label for="country">${_("Country")}</label>
<select id="country" name="country" ${'required aria-required="true"' if settings.REGISTRATION_EXTRA_FIELDS['country'] == 'required' else ''}>
<option value="">--</option>
%for code, country_name in COUNTRIES:
%for code, country_name in sorted(countries.countries, key=lambda (__, name): unicode(name)):
<option value="${code}">${ unicode(country_name) }</option>
%endfor
</select>

View File

@@ -3,7 +3,7 @@
<%namespace name='static' file='static_content.html'/>
<%! from django.conf import settings %>
<%! from django.core.urlresolvers import reverse %>
<%! from django_countries.countries import COUNTRIES %>
<%! from django_countries import countries %>
<%! from student.models import UserProfile %>
<%! from datetime import date %>
<%! import calendar %>

View File

@@ -1,14 +1,19 @@
<form id="email-change-form" method="post">
<label for="new-email"><%- gettext('New Address') %></label>
<label for="new-email"><%- gettext("New Address") %></label>
<input id="new-email" type="text" name="new-email" value="" placeholder="xsy@edx.org" data-validate="required email"/>
<div id="new-email-status" />
<label for="password"><%- gettext('Password') %></label>
<label for="password"><%- gettext("Password") %></label>
<input id="password" type="password" name="password" value="" data-validate="required"/>
<div id="password-status" />
<div class="submit-button">
<input type="submit" id="email-change-submit" value="<%- gettext('Change My Email Address') %>">
<input type="submit" id="email-change-submit" value="<%- gettext("Change My Email Address") %>">
</div>
<div id="request-email-status" />
<div id="password-reset">
<a href="#"><%- gettext("Reset Password") %></a>
</div>
<div id="password-reset-status" />
</form>

View File

@@ -23,4 +23,4 @@
<p>This is a placeholder for the student's account page.</p>
<div id="account-container" />
<div id="account-container"></div>

View File

@@ -19,7 +19,7 @@
% endfor
</%block>
<h1>${_("Student Profile")}</h1>
<h1>Student Profile</h1>
<p>This is a placeholder for the student's profile page.</p>

View File

@@ -1,89 +1,40 @@
<%! from django.utils.translation import ugettext as _ %>
## TODO (ECOM-16): This is part of an AB-test of auto-registration.
## Once the test completes, we can make the winning configuration the default
## and remove this flag.
%if not autoreg:
<!-- /experiment-control -->
<header class="page-header">
<h2 class="title">
%if upgrade:
<span class="sts-label">${_("You are upgrading your registration for")}</span>
%elif reverify:
<span class="sts-label">${_("You are re-verifying for")}</span>
%elif modes_dict and "professional" in modes_dict:
<span class="sts-label">${_("You are registering for")}</span>
%else:
<span class="sts-label">${_("You are registering for")}</span>
%endif
<span class="wrapper-sts">
<span class="sts-course">
<span class="sts-course-org">${course_org}</span>
<span class="sts-course-number">${course_num}</span>
<span class="sts-course-name">${course_name}</span>
</span>
%if modes_dict and "professional" in modes_dict:
<span class="sts-track professional-ed">
<span class="sts-track-value">
${_("Professional Education")}
<h2 class="title">
<span class="wrapper-sts">
% if upgrade:
<span class="sts-label">${_("You are upgrading your registration for")}</span>
% elif reverify:
<span class="sts-label">${_("You are re-verifying for")}</span>
% elif modes_dict and "professional" in modes_dict:
<span class="sts-label">${_("You are registering for")}</span>
% else:
<span class="sts-label">${_("Congrats! You are now registered to audit")}</span>
% endif
<span class="sts-course-org">${course_org}'s</span>
<span class="sts-course-number">${course_num}</span>
<span class="sts-course-name">${course_name}</span>
</span>
</span>
%else:
<span class="sts-track">
<span class="sts-track-value">
%if upgrade:
<span class="context">${_("Upgrading to:")}</span> ${_("Verified")}
%elif reverify:
<span class="context">${_("Re-verifying for:")}</span> ${_("Verified")}
%else:
<span class="context">${_("Registering as: ")}</span> ${_("Verified")}
%endif
</span>
</span>
%endif
</span>
</h2>
</header>
%else:
<!-- /experiment-variant-A -->
<header class="page-header exp-variant-A">
<h2 class="title">
<span class="wrapper-sts">
%if upgrade:
<span class="sts-label">${_("You are upgrading your registration for")}</span>
%elif reverify:
<span class="sts-label">${_("You are re-verifying for")}</span>
%elif modes_dict and "professional" in modes_dict:
<span class="sts-label">${_("You are registering for")}</span>
%else:
<span class="sts-label">${_("Congrats! You are now registered to audit")}</span>
%endif
<span class="sts-course-org">${course_org}'s</span>
<span class="sts-course-number">${course_num}</span>
<span class="sts-course-name">${course_name}.</span>
</span>
%if modes_dict and "professional" in modes_dict:
<span class="sts-track professional-ed">
<span class="sts-track-value">
${_("Professional Education")}
</span>
</span>
%else:
<span class="sts-track">
<span class="sts-track-value">
%if upgrade:
<span class="context">${_("Upgrading to:")}</span> ${_("Verified")}
%elif reverify:
<span class="context">${_("Re-verifying for:")}</span> ${_("Verified")}
%else:
<span class="context">${_("Registering as: ")}</span> ${_("Verified")}
%endif
</span>
%endif
</h2>
% if modes_dict and "professional" in modes_dict:
<span class="sts-track professional-ed">
<span class="sts-track-value">
${_("Professional Education")}
</span>
</span>
% else:
<span class="sts-track">
<span class="sts-track-value">
% if upgrade:
<span class="context">${_("Upgrading to:")}</span> ${_("Verified")}
% elif reverify:
<span class="context">${_("Re-verifying for:")}</span> ${_("Verified")}
% else:
<span class="context">${_("Registering as: ")}</span> ${_("Verified")}
% endif
</span>
</span>
% endif
</h2>
</header>
%endif

View File

@@ -1,65 +1,21 @@
<%! from django.utils.translation import ugettext as _ %>
## TODO (ECOM-16): This is part of an AB-test of auto-registration.
## Once the test completes, we can make the winning configuration the default
## and remove this flag.
%if not autoreg:
<!-- /experiment-control -->
<div class="wrapper-content-supplementary">
<aside class="content-supplementary">
<ul class="list-help">
<li class="help-item help-item-questions">
<h3 class="title">${_("Have questions?")}</h3>
<div class="copy">
<p>${_("Please read {a_start}our FAQs to view common questions about our certificates{a_end}.").format(a_start='<a rel="external" href="'+ marketing_link('WHAT_IS_VERIFIED_CERT') + '">', a_end="</a>")}</p>
</div>
</li>
<aside class="content-supplementary">
<ul class="list-help">
<li class="help-item help-item-questions">
<h3 class="title">${_("Have questions?")}</h3>
<div class="copy">
<p>${_("Please read {a_start}our FAQs to view common questions about our certificates{a_end}.").format(a_start='<a rel="external" href="'+ marketing_link('WHAT_IS_VERIFIED_CERT') + '">', a_end="</a>")}</p>
</div>
</li>
%if can_audit:
<li class="help-item help-item-coldfeet">
%if upgrade:
<h3 class="title">${_("Change your mind?")}</h3>
<div class="copy">
<p>${_("You can always continue to audit the course without verifying.")}</p>
</div>
%else:
<h3 class="title">${_("Change your mind?")}</h3>
<div class="copy">
<p>${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="{}">'.format(course_modes_choose_url), a_end="</a>")}</p>
</div>
%endif
</li>
%endif
<li class="help-item help-item-technical">
<h3 class="title">${_("Technical Requirements")}</h3>
<div class="copy">
<p>${_("Please make sure your browser is updated to the {a_start}most recent version possible{a_end}. Also, please make sure your <strong>webcam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).</strong>").format(a_start='<strong><a rel="external" href="http://browsehappy.com/">', a_end="</a></strong>")}</p>
</div>
</li>
</ul>
</aside>
<li class="help-item help-item-technical">
<h3 class="title">${_("Technical Requirements")}</h3>
<div class="copy">
<p>${_("Please make sure your browser is updated to the {a_start}most recent version possible{a_end}. Also, please make sure your <strong>webcam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).</strong>").format(a_start='<strong><a rel="external" href="http://browsehappy.com/">', a_end="</a></strong>")}</p>
</div>
</li>
</ul>
</aside>
</div> <!-- /wrapper-content-supplementary -->
%else:
<!-- /experiment-variant-A -->
<div class="wrapper-content-supplementary exp-variant-A">
<aside class="content-supplementary">
<ul class="list-help">
<li class="help-item help-item-questions">
<h3 class="title">${_("Have questions?")}</h3>
<div class="copy">
<p>${_("Please read {a_start}our FAQs to view common questions about our certificates{a_end}.").format(a_start='<a rel="external" href="'+ marketing_link('WHAT_IS_VERIFIED_CERT') + '">', a_end="</a>")}</p>
</div>
</li>
<li class="help-item help-item-technical">
<h3 class="title">${_("Technical Requirements")}</h3>
<div class="copy">
<p>${_("Please make sure your browser is updated to the {a_start}most recent version possible{a_end}. Also, please make sure your <strong>webcam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).</strong>").format(a_start='<strong><a rel="external" href="http://browsehappy.com/">', a_end="</a></strong>")}</p>
</div>
</li>
</ul>
</aside>
</div> <!-- /wrapper-content-supplementary -->
%endif

View File

@@ -85,8 +85,7 @@
<a href="#" class="quality-control is-hidden" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-
disabled="false">${_('Turn off captions')}</a>
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-disabled="false">${_('Turn off captions')}</a>
</div>
</div>
</div>

View File

@@ -241,17 +241,6 @@ if settings.COURSEWARE_ENABLED:
'student.views.change_enrollment', name="change_enrollment"),
url(r'^change_email_settings$', 'student.views.change_email_settings', name="change_email_settings"),
# Used for an AB-test of auto-registration
# TODO (ECOM-16): Based on the AB-test, update the default behavior and change
# this URL to point to the original view. Eventually, this URL
# should be removed, but not the AB test completes.
url(
r'^change_enrollment_autoreg$',
'student.views.change_enrollment',
{'auto_register': True},
name="change_enrollment_autoreg",
),
#About the course
url(r'^courses/{}/about$'.format(settings.COURSE_ID_PATTERN),
'courseware.views.course_about', name="about_course"),

View File

@@ -1,3 +1,7 @@
# Patch the xml libs
from safe_lxml import defuse_xml_libs
defuse_xml_libs()
# Disable PyContract contract checking when running as a webserver
import contracts
contracts.disable_all()

View File

@@ -1,3 +1,7 @@
# Patch the xml libs before anything else.
from safe_lxml import defuse_xml_libs
defuse_xml_libs()
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.envs.aws")