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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -40,7 +40,7 @@ def _attr_safe_json(obj):
|
||||
return saxutils.escape(json.dumps(obj), {'"': '"'})
|
||||
|
||||
@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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
224
lms/static/js/dashboard/legacy.js
Normal file
224
lms/static/js/dashboard/legacy.js
Normal 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);
|
||||
@@ -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.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
61
lms/static/sass/application-extend1-rtl.scss.mako
Normal file
61
lms/static/sass/application-extend1-rtl.scss.mako
Normal 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
|
||||
@@ -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*
|
||||
// ====================
|
||||
|
||||
69
lms/static/sass/application-extend2-rtl.scss.mako
Normal file
69
lms/static/sass/application-extend2-rtl.scss.mako
Normal 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
|
||||
@@ -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*
|
||||
// ====================
|
||||
|
||||
57
lms/static/sass/application-rtl.scss.mako
Normal file
57
lms/static/sass/application-rtl.scss.mako
Normal 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
|
||||
@@ -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*
|
||||
// ====================
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
|
||||
// ====================
|
||||
|
||||
|
||||
// extends - UI - used for page/view-level wrappers (for centering/grids)
|
||||
%ui-wrapper {
|
||||
@include clearfix();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// lms variables
|
||||
|
||||
// base
|
||||
$baseline: 20px;
|
||||
|
||||
|
||||
77
lms/static/sass/course-rtl.scss.mako
Normal file
77
lms/static/sass/course-rtl.scss.mako
Normal 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";
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
@mixin blue-button {
|
||||
display: block;
|
||||
height: 35px;
|
||||
@@ -54,4 +55,4 @@
|
||||
&:hover, &:focus {
|
||||
background: -webkit-linear-gradient(top, #888, #666);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,3 +185,8 @@
|
||||
|
||||
}
|
||||
|
||||
.rtl .instructor-dashboard-wrapper .beta-button-wrapper,
|
||||
.rtl .instructor-dashboard-wrapper .studio-edit-link {
|
||||
left: 2em;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ nav.course-material {
|
||||
padding: 10px 0 0 0;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
@include float(left);
|
||||
list-style: none;
|
||||
|
||||
a {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
11
lms/static/sass/vendor/bi-app/_bi-app-ltr.scss
vendored
Executable file
11
lms/static/sass/vendor/bi-app/_bi-app-ltr.scss
vendored
Executable 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';
|
||||
11
lms/static/sass/vendor/bi-app/_bi-app-rtl.scss
vendored
Executable file
11
lms/static/sass/vendor/bi-app/_bi-app-rtl.scss
vendored
Executable 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
294
lms/static/sass/vendor/bi-app/_mixins.scss
vendored
Executable 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;
|
||||
}
|
||||
}
|
||||
15
lms/static/sass/vendor/bi-app/_variables-ltr.scss
vendored
Executable file
15
lms/static/sass/vendor/bi-app/_variables-ltr.scss
vendored
Executable 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;
|
||||
15
lms/static/sass/vendor/bi-app/_variables-rtl.scss
vendored
Executable file
15
lms/static/sass/vendor/bi-app/_variables-rtl.scss
vendored
Executable 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;
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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'/>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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"/>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
11
lms/urls.py
11
lms/urls.py
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user