Commit aa172272 by Brian Wilson

Update master with hotfixes from release-2013-10-17.

parents 0138322c aba8fa68
...@@ -51,6 +51,8 @@ node_modules ...@@ -51,6 +51,8 @@ node_modules
*.scssc *.scssc
lms/static/sass/*.css lms/static/sass/*.css
lms/static/sass/application.scss lms/static/sass/application.scss
lms/static/sass/application-extend1.scss
lms/static/sass/application-extend2.scss
lms/static/sass/course.scss lms/static/sass/course.scss
cms/static/sass/*.css cms/static/sass/*.css
......
...@@ -16,6 +16,11 @@ Blades: LTI module can now load external content in a new window. ...@@ -16,6 +16,11 @@ Blades: LTI module can now load external content in a new window.
LMS: Disable data download buttons on the instructor dashboard for large courses LMS: Disable data download buttons on the instructor dashboard for large courses
Common: Adds ability to disable a student's account. Students with disabled
accounts will be prohibited from site access.
LMS: Fix issue with CourseMode expiration dates
LMS: Ported bulk emailing to the beta instructor dashboard. LMS: Ported bulk emailing to the beta instructor dashboard.
LMS: Add monitoring of bulk email subtasks to display progress on instructor dash. LMS: Add monitoring of bulk email subtasks to display progress on instructor dash.
......
...@@ -154,6 +154,7 @@ MIDDLEWARE_CLASSES = ( ...@@ -154,6 +154,7 @@ MIDDLEWARE_CLASSES = (
# Instead of AuthenticationMiddleware, we use a cache-backed version # Instead of AuthenticationMiddleware, we use a cache-backed version
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', 'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
'student.middleware.UserStandingMiddleware',
'contentserver.middleware.StaticContentServer', 'contentserver.middleware.StaticContentServer',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
......
"""
Middleware that checks user standing for the purpose of keeping users with
disabled accounts from accessing the site.
"""
from django.http import HttpResponseForbidden
from django.utils.translation import ugettext as _
from django.conf import settings
from student.models import UserStanding
class UserStandingMiddleware(object):
"""
Checks a user's standing on request. Returns a 403 if the user's
status is 'disabled'.
"""
def process_request(self, request):
user = request.user
try:
user_account = UserStanding.objects.get(user=user.id)
# because user is a unique field in UserStanding, there will either be
# one or zero user_accounts associated with a UserStanding
except UserStanding.DoesNotExist:
pass
else:
if user_account.account_status == UserStanding.ACCOUNT_DISABLED:
msg = _(
'Your account has been disabled. If you believe '
'this was done in error, please contact us at '
'{link_start}{support_email}{link_end}'
).format(
support_email=settings.DEFAULT_FEEDBACK_EMAIL,
link_start=u'<a href="mailto:{address}?subject={subject_line}">'.format(
address=settings.DEFAULT_FEEDBACK_EMAIL,
subject_line=_('Disabled Account'),
),
link_end=u'</a>'
)
return HttpResponseForbidden(msg)
...@@ -33,6 +33,27 @@ from pytz import UTC ...@@ -33,6 +33,27 @@ from pytz import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
class UserStanding(models.Model):
"""
This table contains a student's account's status.
Currently, we're only disabling accounts; in the future we can imagine
taking away more specific privileges, like forums access, or adding
more specific karma levels or probationary stages.
"""
ACCOUNT_DISABLED = "disabled"
ACCOUNT_ENABLED = "enabled"
USER_STANDING_CHOICES = (
(ACCOUNT_DISABLED, u"Account Disabled"),
(ACCOUNT_ENABLED, u"Account Enabled"),
)
user = models.ForeignKey(User, db_index=True, related_name='standing', unique=True)
account_status = models.CharField(
blank=True, max_length=31, choices=USER_STANDING_CHOICES
)
changed_by = models.ForeignKey(User, blank=True)
standing_last_changed_at = models.DateTimeField(auto_now=True)
class UserProfile(models.Model): class UserProfile(models.Model):
"""This is where we store all the user demographic fields. We have a """This is where we store all the user demographic fields. We have a
......
from student.models import (User, UserProfile, Registration, from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed, CourseEnrollment, CourseEnrollmentAllowed, CourseEnrollment,
PendingEmailChange) PendingEmailChange, UserStanding,
)
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
...@@ -16,6 +17,13 @@ class GroupFactory(DjangoModelFactory): ...@@ -16,6 +17,13 @@ class GroupFactory(DjangoModelFactory):
name = u'staff_MITx/999/Robot_Super_Course' name = u'staff_MITx/999/Robot_Super_Course'
class UserStandingFactory(DjangoModelFactory):
FACTORY_FOR = UserStanding
user = None
account_status = None
changed_by = None
class UserProfileFactory(DjangoModelFactory): class UserProfileFactory(DjangoModelFactory):
FACTORY_FOR = UserProfile FACTORY_FOR = UserProfile
......
"""
Unit tests for email feature flag in student dashboard. Additionally tests
that bulk email is always disabled for non-Mongo backed courses, regardless
of email feature flag, and that the view is conditionally available when
Course Auth is turned on.
"""
from django.test.utils import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch
from unittest.case import SkipTest
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from bulk_email.models import CourseAuthorization
from mock import patch
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestStudentDashboardEmailView(ModuleStoreTestCase):
"""
Check for email view displayed with flag
"""
def setUp(self):
self.course = CourseFactory.create()
# Create student account
student = UserFactory.create()
CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
self.client.login(username=student.username, password="test")
try:
# URL for dashboard
self.url = reverse('dashboard')
except NoReverseMatch:
raise SkipTest("Skip this test if url cannot be found (ie running from CMS tests)")
# URL for email settings modal
self.email_modal_link = (
('<a href="#email-settings-modal" class="email-settings" rel="leanModal" '
'data-course-id="{0}/{1}/{2}" data-course-number="{1}" '
'data-optout="False">Email Settings</a>').format(
self.course.org,
self.course.number,
self.course.display_name.replace(' ', '_')
)
)
def tearDown(self):
"""
Undo all patches.
"""
patch.stopall()
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_true(self):
# Assert that the URL for the email view is in the response
response = self.client.get(self.url)
self.assertTrue(self.email_modal_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False})
def test_email_flag_false(self):
# Assert that the URL for the email view is not in the response
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_email_unauthorized(self):
# Assert that instructor email is not enabled for this course
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
# Assert that the URL for the email view is not in the response
# if this course isn't authorized
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_email_authorized(self):
# Authorize the course to use email
cauth = CourseAuthorization(course_id=self.course.id, email_enabled=True)
cauth.save()
# Assert that instructor email is enabled for this course
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
# Assert that the URL for the email view is not in the response
# if this course isn't authorized
response = self.client.get(self.url)
self.assertTrue(self.email_modal_link in response.content)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestStudentDashboardEmailViewXMLBacked(ModuleStoreTestCase):
"""
Check for email view on student dashboard, with XML backed course.
"""
def setUp(self):
self.course_name = 'edX/toy/2012_Fall'
# Create student account
student = UserFactory.create()
CourseEnrollmentFactory.create(user=student, course_id=self.course_name)
self.client.login(username=student.username, password="test")
try:
# URL for dashboard
self.url = reverse('dashboard')
except NoReverseMatch:
raise SkipTest("Skip this test if url cannot be found (ie running from CMS tests)")
# URL for email settings modal
self.email_modal_link = (
('<a href="#email-settings-modal" class="email-settings" rel="leanModal" '
'data-course-id="{0}/{1}/{2}" data-course-number="{1}" '
'data-optout="False">Email Settings</a>').format(
'edX',
'toy',
'2012_Fall'
)
)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_true_xml_store(self):
# The flag is enabled, and since REQUIRE_COURSE_EMAIL_AUTH is False, all courses should
# be authorized to use email. But the course is not Mongo-backed (should not work)
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_false_xml_store(self):
# Email disabled, shouldn't see link.
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
"""
These are tests for disabling and enabling student accounts, and for making sure
that students with disabled accounts are unable to access the courseware.
"""
from student.tests.factories import UserFactory, UserStandingFactory
from student.models import UserStanding
from django.test import TestCase, Client
from django.core.urlresolvers import reverse, NoReverseMatch
from nose.plugins.skip import SkipTest
class UserStandingTest(TestCase):
"""test suite for user standing view for enabling and disabling accounts"""
def setUp(self):
# create users
self.bad_user = UserFactory.create(
username='bad_user',
)
self.good_user = UserFactory.create(
username='good_user',
)
self.non_staff = UserFactory.create(
username='non_staff',
)
self.admin = UserFactory.create(
username='admin',
is_staff=True,
)
# create clients
self.bad_user_client = Client()
self.good_user_client = Client()
self.non_staff_client = Client()
self.admin_client = Client()
for user, client in [
(self.bad_user, self.bad_user_client),
(self.good_user, self.good_user_client),
(self.non_staff, self.non_staff_client),
(self.admin, self.admin_client),
]:
client.login(username=user.username, password='test')
UserStandingFactory.create(
user=self.bad_user,
account_status=UserStanding.ACCOUNT_DISABLED,
changed_by=self.admin
)
# set different stock urls for lms and cms
# to test disabled accounts' access to site
try:
self.some_url = reverse('dashboard')
except NoReverseMatch:
self.some_url = reverse('index')
# since it's only possible to disable accounts from lms, we're going
# to skip tests for cms
def test_disable_account(self):
self.assertEqual(
UserStanding.objects.filter(user=self.good_user).count(), 0
)
try:
response = self.admin_client.post(reverse('disable_account_ajax'), {
'username': self.good_user.username,
'account_action': 'disable',
})
except NoReverseMatch:
raise SkipTest()
self.assertEqual(
UserStanding.objects.get(user=self.good_user).account_status,
UserStanding.ACCOUNT_DISABLED
)
def test_disabled_account_403s(self):
response = self.bad_user_client.get(self.some_url)
self.assertEqual(response.status_code, 403)
def test_reenable_account(self):
try:
response = self.admin_client.post(reverse('disable_account_ajax'), {
'username': self.bad_user.username,
'account_action': 'reenable'
})
except NoReverseMatch:
raise SkipTest()
self.assertEqual(
UserStanding.objects.get(user=self.bad_user).account_status,
UserStanding.ACCOUNT_ENABLED
)
def test_non_staff_cant_access_disable_view(self):
try:
response = self.non_staff_client.get(reverse('manage_user_standing'), {
'user': self.non_staff,
})
except NoReverseMatch:
raise SkipTest()
self.assertEqual(response.status_code, 404)
def test_non_staff_cant_disable_account(self):
try:
response = self.non_staff_client.post(reverse('disable_account_ajax'), {
'username': self.good_user.username,
'user': self.non_staff,
'account_action': 'disable'
})
except NoReverseMatch:
raise SkipTest()
self.assertEqual(response.status_code, 404)
self.assertEqual(
UserStanding.objects.filter(user=self.good_user).count(), 0
)
...@@ -16,6 +16,7 @@ from django.contrib.auth import logout, authenticate, login ...@@ -16,6 +16,7 @@ from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import password_reset_confirm from django.contrib.auth.views import password_reset_confirm
# from django.contrib.sessions.models import Session
from django.core.cache import cache from django.core.cache import cache
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
...@@ -29,18 +30,21 @@ from django.shortcuts import redirect ...@@ -29,18 +30,21 @@ from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.utils.http import cookie_date, base36_to_int, urlencode from django.utils.http import cookie_date, base36_to_int, urlencode
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST, require_GET
from django.contrib.admin.views.decorators import staff_member_required
from django.utils.translation import ugettext as _u
from ratelimitbackend.exceptions import RateLimitException from ratelimitbackend.exceptions import RateLimitException
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from course_modes.models import CourseMode from course_modes.models import CourseMode
from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm, from student.models import (
TestCenterRegistration, TestCenterRegistrationForm, Registration, UserProfile, TestCenterUser, TestCenterUserForm,
PendingNameChange, PendingEmailChange, TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange,
CourseEnrollment, unique_id_for_user, PendingEmailChange, CourseEnrollment, unique_id_for_user,
get_testcenter_registration, CourseEnrollmentAllowed) get_testcenter_registration, CourseEnrollmentAllowed, UserStanding,
)
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
...@@ -58,7 +62,7 @@ from courseware.access import has_access ...@@ -58,7 +62,7 @@ from courseware.access import has_access
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
import external_auth.views import external_auth.views
from bulk_email.models import Optout from bulk_email.models import Optout, CourseAuthorization
import shoppingcart import shoppingcart
import track.views import track.views
...@@ -66,6 +70,8 @@ import track.views ...@@ -66,6 +70,8 @@ import track.views
from dogapi import dog_stats_api from dogapi import dog_stats_api
from pytz import UTC from pytz import UTC
from util.json_request import JsonResponse
log = logging.getLogger("mitx.student") log = logging.getLogger("mitx.student")
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
...@@ -297,10 +303,13 @@ def dashboard(request): ...@@ -297,10 +303,13 @@ def dashboard(request):
cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in courses} cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in courses}
# only show email settings for Mongo course and when bulk email is turned on # only show email settings for Mongo course and when bulk email is turned on
show_email_settings_for = frozenset(course.id for course, _enrollment in courses show_email_settings_for = frozenset(
if (settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and course.id for course, _enrollment in courses if (
modulestore().get_modulestore_type(course.id) == MONGO_MODULESTORE_TYPE)) settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and
modulestore().get_modulestore_type(course.id) == MONGO_MODULESTORE_TYPE and
CourseAuthorization.instructor_email_enabled(course.id)
)
)
# get info w.r.t ExternalAuthMap # get info w.r.t ExternalAuthMap
external_auth_map = None external_auth_map = None
try: try:
...@@ -601,6 +610,81 @@ def logout_user(request): ...@@ -601,6 +610,81 @@ def logout_user(request):
domain=settings.SESSION_COOKIE_DOMAIN) domain=settings.SESSION_COOKIE_DOMAIN)
return response return response
@require_GET
@login_required
@ensure_csrf_cookie
def manage_user_standing(request):
"""
Renders the view used to manage user standing. Also displays a table
of user accounts that have been disabled and who disabled them.
"""
if not request.user.is_staff:
raise Http404
all_disabled_accounts = UserStanding.objects.filter(
account_status=UserStanding.ACCOUNT_DISABLED
)
all_disabled_users = [standing.user for standing in all_disabled_accounts]
headers = ['username', 'account_changed_by']
rows = []
for user in all_disabled_users:
row = [user.username, user.standing.all()[0].changed_by]
rows.append(row)
context = {'headers': headers, 'rows': rows}
return render_to_response("manage_user_standing.html", context)
@require_POST
@login_required
@ensure_csrf_cookie
def disable_account_ajax(request):
"""
Ajax call to change user standing. Endpoint of the form
in manage_user_standing.html
"""
if not request.user.is_staff:
raise Http404
username = request.POST.get('username')
context = {}
if username is None or username.strip() == '':
context['message'] = _u('Please enter a username')
return JsonResponse(context, status=400)
account_action = request.POST.get('account_action')
if account_action is None:
context['message'] = _u('Please choose an option')
return JsonResponse(context, status=400)
username = username.strip()
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
context['message'] = _u("User with username {} does not exist").format(username)
return JsonResponse(context, status=400)
else:
user_account, _ = UserStanding.objects.get_or_create(
user=user, defaults={'changed_by': request.user},
)
if account_action == 'disable':
user_account.account_status = UserStanding.ACCOUNT_DISABLED
context['message'] = _u("Successfully disabled {}'s account").format(username)
log.info("{} disabled {}'s account".format(request.user, username))
elif account_action == 'reenable':
user_account.account_status = UserStanding.ACCOUNT_ENABLED
context['message'] = _u("Successfully reenabled {}'s account").format(username)
log.info("{} reenabled {}'s account".format(request.user, username))
else:
context['message'] = _u("Unexpected account status")
return JsonResponse(context, status=400)
user_account.changed_by = request.user
user_account.standing_last_changed_at = datetime.datetime.now(UTC)
user_account.save()
return JsonResponse(context)
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -66,6 +66,21 @@ def add_to_course_staff(username, course_num): ...@@ -66,6 +66,21 @@ def add_to_course_staff(username, course_num):
@world.absorb @world.absorb
def add_to_course_staff(username, course_num):
"""
Add the user with `username` to the course staff group
for `course_num`.
"""
# Based on code in lms/djangoapps/courseware/access.py
group_name = "instructor_{}".format(course_num)
group, _ = Group.objects.get_or_create(name=group_name)
group.save()
user = User.objects.get(username=username)
user.groups.add(group)
@world.absorb
def clear_courses(): def clear_courses():
# Flush and initialize the module store # Flush and initialize the module store
# Note that if your test module gets in some weird state # Note that if your test module gets in some weird state
......
...@@ -638,6 +638,8 @@ div.video { ...@@ -638,6 +638,8 @@ div.video {
ol.subtitles { ol.subtitles {
width: 0; width: 0;
height: 0; height: 0;
visibility: hidden;
} }
ol.subtitles.html5 { ol.subtitles.html5 {
...@@ -671,6 +673,8 @@ div.video { ...@@ -671,6 +673,8 @@ div.video {
ol.subtitles { ol.subtitles {
right: -(flex-grid(4)); right: -(flex-grid(4));
width: auto; width: auto;
visibility: hidden;
} }
} }
......
...@@ -318,9 +318,12 @@ def send_course_email(entry_id, email_id, to_list, global_email_context, subtask ...@@ -318,9 +318,12 @@ def send_course_email(entry_id, email_id, to_list, global_email_context, subtask
# Check that the requested subtask is actually known to the current InstructorTask entry. # Check that the requested subtask is actually known to the current InstructorTask entry.
# If this fails, it throws an exception, which should fail this subtask immediately. # If this fails, it throws an exception, which should fail this subtask immediately.
# This can happen when the parent task has been run twice, and results in duplicate # This can happen when the parent task has been run twice, and results in duplicate
# subtasks being created for the same InstructorTask entry. We hope to catch this condition # subtasks being created for the same InstructorTask entry. This can happen when Celery
# in perform_delegate_email_batches(), but just in case we fail to do so there, # loses its connection to its broker, and any current tasks get requeued.
# we check here as well. # We hope to catch this condition in perform_delegate_email_batches() when it's the parent
# task that is resubmitted, but just in case we fail to do so there, we check here as well.
# There is also a possibility that this task will be run twice by Celery, for the same reason.
# To deal with that, we need to confirm that the task has not already been completed.
check_subtask_is_valid(entry_id, current_task_id) check_subtask_is_valid(entry_id, current_task_id)
send_exception = None send_exception = None
......
...@@ -5,6 +5,8 @@ from itertools import cycle ...@@ -5,6 +5,8 @@ from itertools import cycle
from mock import patch from mock import patch
from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError
from celery.states import SUCCESS
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
...@@ -19,7 +21,12 @@ from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentF ...@@ -19,7 +21,12 @@ from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentF
from bulk_email.models import CourseEmail, SEND_TO_ALL from bulk_email.models import CourseEmail, SEND_TO_ALL
from bulk_email.tasks import perform_delegate_email_batches, send_course_email from bulk_email.tasks import perform_delegate_email_batches, send_course_email
from instructor_task.models import InstructorTask from instructor_task.models import InstructorTask
from instructor_task.subtasks import create_subtask_status, initialize_subtask_info from instructor_task.subtasks import (
create_subtask_status,
initialize_subtask_info,
update_subtask_status,
DuplicateTaskException,
)
class EmailTestException(Exception): class EmailTestException(Exception):
...@@ -210,7 +217,7 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -210,7 +217,7 @@ class TestEmailErrors(ModuleStoreTestCase):
subtask_id = "subtask-id-value" subtask_id = "subtask-id-value"
subtask_status = create_subtask_status(subtask_id) subtask_status = create_subtask_status(subtask_id)
email_id = 1001 email_id = 1001
with self.assertRaisesRegexp(ValueError, 'unable to find email subtasks of instructor task'): with self.assertRaisesRegexp(DuplicateTaskException, 'unable to find email subtasks of instructor task'):
send_course_email(entry_id, email_id, to_list, global_email_context, subtask_status) send_course_email(entry_id, email_id, to_list, global_email_context, subtask_status)
def test_send_email_missing_subtask(self): def test_send_email_missing_subtask(self):
...@@ -224,9 +231,24 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -224,9 +231,24 @@ class TestEmailErrors(ModuleStoreTestCase):
different_subtask_id = "bogus-subtask-id-value" different_subtask_id = "bogus-subtask-id-value"
subtask_status = create_subtask_status(different_subtask_id) subtask_status = create_subtask_status(different_subtask_id)
bogus_email_id = 1001 bogus_email_id = 1001
with self.assertRaisesRegexp(ValueError, 'unable to find status for email subtask of instructor task'): with self.assertRaisesRegexp(DuplicateTaskException, 'unable to find status for email subtask of instructor task'):
send_course_email(entry_id, bogus_email_id, to_list, global_email_context, subtask_status) send_course_email(entry_id, bogus_email_id, to_list, global_email_context, subtask_status)
def test_send_email_completed_subtask(self):
# test at a lower level, to ensure that the course gets checked down below too.
entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor)
entry_id = entry.id # pylint: disable=E1101
subtask_id = "subtask-id-value"
initialize_subtask_info(entry, "emailed", 100, [subtask_id])
subtask_status = create_subtask_status(subtask_id, state=SUCCESS)
update_subtask_status(entry_id, subtask_id, subtask_status)
bogus_email_id = 1001
to_list = ['test@test.com']
global_email_context = {'course_title': 'dummy course'}
new_subtask_status = create_subtask_status(subtask_id)
with self.assertRaisesRegexp(DuplicateTaskException, 'already completed'):
send_course_email(entry_id, bogus_email_id, to_list, global_email_context, new_subtask_status)
def dont_test_send_email_undefined_email(self): def dont_test_send_email_undefined_email(self):
# test at a lower level, to ensure that the course gets checked down below too. # test at a lower level, to ensure that the course gets checked down below too.
entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor)
......
""" """
Unit tests for email feature flag in new instructor dashboard. Unit tests for email feature flag in new instructor dashboard.
Additionally tests that bulk email is always disabled for Additionally tests that bulk email is always disabled for
non-Mongo backed courses, regardless of email feature flag. non-Mongo backed courses, regardless of email feature flag, and
that the view is conditionally available when Course Auth is turned on.
""" """
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -90,7 +91,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase): ...@@ -90,7 +91,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase):
# Assert that instructor email is enabled for this course # Assert that instructor email is enabled for this course
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id)) self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
# Assert that the URL for the email view is not in the response # Assert that the URL for the email view is in the response
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertTrue(self.email_link in response.content) self.assertTrue(self.email_link in response.content)
......
""" """
Unit tests for email feature flag in legacy instructor dashboard Unit tests for email feature flag in legacy instructor dashboard.
and student dashboard. Additionally tests that bulk email Additionally tests that bulk email is always disabled for non-Mongo
is always disabled for non-Mongo backed courses, regardless backed courses, regardless of email feature flag, and that the
of email feature flag. view is conditionally available when Course Auth is turned on.
""" """
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory from student.tests.factories import AdminFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore import XML_MODULESTORE_TYPE from xmodule.modulestore import XML_MODULESTORE_TYPE
from bulk_email.models import CourseAuthorization
from mock import patch from mock import patch
...@@ -59,70 +60,37 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase): ...@@ -59,70 +60,37 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
send_to_label = '<label for="id_to">Send to:</label>' send_to_label = '<label for="id_to">Send to:</label>'
self.assertTrue(send_to_label in response.content) self.assertTrue(send_to_label in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False}) @patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_email_flag_false(self): def test_email_flag_unauthorized(self):
# Assert that the URL for the email view is not in the response # Assert that the URL for the email view is not in the response
# email is enabled, but this course is not authorized to send email
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertFalse(self.email_link in response.content) self.assertFalse(self.email_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True}) @patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_email_flag_true_xml_store(self): def test_email_flag_authorized(self):
# If the enable email setting is enabled, but this is an XML backed course, # Assert that the URL for the email view is in the response
# the email view shouldn't be available on the instructor dashboard. # email is enabled, and this course is authorized to send email
# The course factory uses a MongoModuleStore backing, so patch the
# `get_modulestore_type` method to pretend to be XML-backed.
# This is OK; we're simply testing that the `is_mongo_modulestore_type` flag
# in `instructor/views/legacy.py` is doing the correct thing.
with patch('xmodule.modulestore.mongo.base.MongoModuleStore.get_modulestore_type') as mock_modulestore:
mock_modulestore.return_value = XML_MODULESTORE_TYPE
# Assert that the URL for the email view is not in the response
response = self.client.get(self.url)
self.assertFalse(self.email_link in response.content)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) # Assert that instructor email is not enabled for this course
class TestStudentDashboardEmailView(ModuleStoreTestCase): self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
""" response = self.client.get(self.url)
Check for email view displayed with flag self.assertFalse(self.email_link in response.content)
"""
def setUp(self):
self.course = CourseFactory.create()
# Create student account # Authorize the course to use email
student = UserFactory.create() cauth = CourseAuthorization(course_id=self.course.id, email_enabled=True)
CourseEnrollmentFactory.create(user=student, course_id=self.course.id) cauth.save()
self.client.login(username=student.username, password="test")
# URL for dashboard
self.url = reverse('dashboard')
# URL for email settings modal
self.email_modal_link = (('<a href="#email-settings-modal" class="email-settings" rel="leanModal" '
'data-course-id="{0}/{1}/{2}" data-course-number="{1}" '
'data-optout="False">Email Settings</a>')
.format(self.course.org,
self.course.number,
self.course.display_name.replace(' ', '_')))
def tearDown(self): # Assert that instructor email is enabled for this course
""" self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
Undo all patches.
"""
patch.stopall()
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_email_flag_true(self):
# Assert that the URL for the email view is in the response
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertTrue(self.email_modal_link in response.content) self.assertTrue(self.email_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False}) @patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False})
def test_email_flag_false(self): def test_email_flag_false(self):
# Assert that the URL for the email view is not in the response # Assert that the URL for the email view is not in the response
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content) self.assertFalse(self.email_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True}) @patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_email_flag_true_xml_store(self): def test_email_flag_true_xml_store(self):
...@@ -139,4 +107,4 @@ class TestStudentDashboardEmailView(ModuleStoreTestCase): ...@@ -139,4 +107,4 @@ class TestStudentDashboardEmailView(ModuleStoreTestCase):
# Assert that the URL for the email view is not in the response # Assert that the URL for the email view is not in the response
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content) self.assertFalse(self.email_link in response.content)
...@@ -726,18 +726,31 @@ def instructor_dashboard(request, course_id): ...@@ -726,18 +726,31 @@ def instructor_dashboard(request, course_id):
email_subject = request.POST.get("subject") email_subject = request.POST.get("subject")
html_message = request.POST.get("message") html_message = request.POST.get("message")
# Create the CourseEmail object. This is saved immediately, so that try:
# any transaction that has been pending up to this point will also be # Create the CourseEmail object. This is saved immediately, so that
# committed. # any transaction that has been pending up to this point will also be
email = CourseEmail.create(course_id, request.user, email_to_option, email_subject, html_message) # committed.
email = CourseEmail.create(course_id, request.user, email_to_option, email_subject, html_message)
# Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes)
submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101
# Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes) except Exception as err:
submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101 # Catch any errors and deliver a message to the user
error_msg = "Failed to send email! ({0})".format(err)
msg += "<font color='red'>" + error_msg + "</font>"
log.exception(error_msg)
if email_to_option == "all":
email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending. Please note that for large public classes (~10k), it may take 1-2 hours to send all emails.</p></div>'
else: else:
email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending.</p></div>' # If sending the task succeeds, deliver a success message to the user.
if email_to_option == "all":
email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending. Please note that for large public classes (~10k), it may take 1-2 hours to send all emails.</p></div>'
else:
email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending.</p></div>'
elif "Show Background Email Task History" in action:
message, datatable = get_background_task_table(course_id, task_type='bulk_course_email')
msg += message
elif "Show Background Email Task History" in action: elif "Show Background Email Task History" in action:
message, datatable = get_background_task_table(course_id, task_type='bulk_course_email') message, datatable = get_background_task_table(course_id, task_type='bulk_course_email')
......
...@@ -14,6 +14,11 @@ from instructor_task.models import InstructorTask, PROGRESS, QUEUING ...@@ -14,6 +14,11 @@ from instructor_task.models import InstructorTask, PROGRESS, QUEUING
TASK_LOG = get_task_logger(__name__) TASK_LOG = get_task_logger(__name__)
class DuplicateTaskException(Exception):
"""Exception indicating that a task already exists or has already completed."""
pass
def create_subtask_status(task_id, succeeded=0, failed=0, skipped=0, retried_nomax=0, retried_withmax=0, state=None): def create_subtask_status(task_id, succeeded=0, failed=0, skipped=0, retried_nomax=0, retried_withmax=0, state=None):
""" """
Create and return a dict for tracking the status of a subtask. Create and return a dict for tracking the status of a subtask.
...@@ -149,30 +154,48 @@ def initialize_subtask_info(entry, action_name, total_num, subtask_id_list): ...@@ -149,30 +154,48 @@ def initialize_subtask_info(entry, action_name, total_num, subtask_id_list):
def check_subtask_is_valid(entry_id, current_task_id): def check_subtask_is_valid(entry_id, current_task_id):
""" """
Confirms that the current subtask is known to the InstructorTask. Confirms that the current subtask is known to the InstructorTask and hasn't already been completed.
Problems can occur when the parent task has been run twice, and results in duplicate
subtasks being created for the same InstructorTask entry. This can happen when Celery
loses its connection to its broker, and any current tasks get requeued.
This may happen if a task that spawns subtasks is called twice with If a parent task gets requeued, then the same InstructorTask may have a different set of
the same task_id and InstructorTask entry_id. The set of subtasks subtasks defined (to do the same thing), so the subtasks from the first queuing would not
that are recorded in the InstructorTask from the first call get clobbered be known to the InstructorTask. We return an exception in this case.
by the the second set of subtasks. So when the first set of subtasks
actually run, they won't be found in the InstructorTask.
Raises a ValueError exception if not. If a subtask gets requeued, then the first time the subtask runs it should run fine to completion.
However, we want to prevent it from running again, so we check here to see what the existing
subtask's status is. If it is complete, we return an exception.
Raises a DuplicateTaskException exception if it's not a task that should be run.
""" """
# Confirm that the InstructorTask actually defines subtasks.
entry = InstructorTask.objects.get(pk=entry_id) entry = InstructorTask.objects.get(pk=entry_id)
if len(entry.subtasks) == 0: if len(entry.subtasks) == 0:
format_str = "Unexpected task_id '{}': unable to find email subtasks of instructor task '{}'" format_str = "Unexpected task_id '{}': unable to find email subtasks of instructor task '{}'"
msg = format_str.format(current_task_id, entry) msg = format_str.format(current_task_id, entry)
TASK_LOG.warning(msg) TASK_LOG.warning(msg)
raise ValueError(msg) raise DuplicateTaskException(msg)
# Confirm that the InstructorTask knows about this particular subtask.
subtask_dict = json.loads(entry.subtasks) subtask_dict = json.loads(entry.subtasks)
subtask_status_info = subtask_dict['status'] subtask_status_info = subtask_dict['status']
if current_task_id not in subtask_status_info: if current_task_id not in subtask_status_info:
format_str = "Unexpected task_id '{}': unable to find status for email subtask of instructor task '{}'" format_str = "Unexpected task_id '{}': unable to find status for email subtask of instructor task '{}'"
msg = format_str.format(current_task_id, entry) msg = format_str.format(current_task_id, entry)
TASK_LOG.warning(msg) TASK_LOG.warning(msg)
raise ValueError(msg) raise DuplicateTaskException(msg)
# Confirm that the InstructorTask doesn't think that this subtask has already been
# performed successfully.
subtask_status = subtask_status_info[current_task_id]
subtask_state = subtask_status.get('state')
if subtask_state in READY_STATES:
format_str = "Unexpected task_id '{}': already completed - status {} for email subtask of instructor task '{}'"
msg = format_str.format(current_task_id, subtask_status, entry)
TASK_LOG.warning(msg)
raise DuplicateTaskException(msg)
@transaction.commit_manually @transaction.commit_manually
......
...@@ -586,6 +586,7 @@ MIDDLEWARE_CLASSES = ( ...@@ -586,6 +586,7 @@ MIDDLEWARE_CLASSES = (
# Instead of AuthenticationMiddleware, we use a cached backed version # Instead of AuthenticationMiddleware, we use a cached backed version
#'django.contrib.auth.middleware.AuthenticationMiddleware', #'django.contrib.auth.middleware.AuthenticationMiddleware',
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', 'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
'student.middleware.UserStandingMiddleware',
'contentserver.middleware.StaticContentServer', 'contentserver.middleware.StaticContentServer',
'crum.CurrentRequestUserMiddleware', 'crum.CurrentRequestUserMiddleware',
...@@ -647,25 +648,49 @@ open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_end ...@@ -647,25 +648,49 @@ open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_end
notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.coffee')) notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.coffee'))
PIPELINE_CSS = { PIPELINE_CSS = {
'application': { 'style-vendor': {
'source_filenames': ['sass/application.css'], 'source_filenames': [
'output_filename': 'css/lms-application.css', 'css/vendor/font-awesome.css',
'css/vendor/jquery.qtip.min.css',
'css/vendor/responsive-carousel/responsive-carousel.css',
'css/vendor/responsive-carousel/responsive-carousel.slide.css',
],
'output_filename': 'css/lms-style-vendor.css',
},
'style-app': {
'source_filenames': [
'sass/application.css',
'sass/ie.css'
],
'output_filename': 'css/lms-style-app.css',
},
'style-app-extend1': {
'source_filenames': [
'sass/application-extend1.css',
],
'output_filename': 'css/lms-style-app-extend1.css',
}, },
'course': { 'style-app-extend2': {
'source_filenames': [
'sass/application-extend2.css',
],
'output_filename': 'css/lms-style-app-extend2.css',
},
'style-course-vendor': {
'source_filenames': [ 'source_filenames': [
'js/vendor/CodeMirror/codemirror.css', 'js/vendor/CodeMirror/codemirror.css',
'css/vendor/jquery.treeview.css', 'css/vendor/jquery.treeview.css',
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/jquery.qtip.min.css',
'css/vendor/annotator.min.css', 'css/vendor/annotator.min.css',
],
'output_filename': 'css/lms-style-course-vendor.css',
},
'style-course': {
'source_filenames': [
'sass/course.css', 'sass/course.css',
'xmodule/modules.css', 'xmodule/modules.css',
], ],
'output_filename': 'css/lms-course.css', 'output_filename': 'css/lms-style-course.css',
},
'ie-fixes': {
'source_filenames': ['sass/ie.css'],
'output_filename': 'css/lms-ie.css',
}, },
} }
......
...@@ -242,3 +242,10 @@ footer .references { ...@@ -242,3 +242,10 @@ footer .references {
outline: thin dotted !important; outline: thin dotted !important;
} }
} }
// ====================
// poor cascade made worse by CSS splitting requires us to redefine the dashboard views' visual top padding
.dashboard {
padding-top: 60px;
}
## 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
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'base/reset';
@import 'base/mixins';
@import 'base/variables';
## 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.get('THEME_NAME') is not None:
// 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/testcenter-register';
@import 'multicourse/courses';
@import 'multicourse/course_about';
@import 'multicourse/jobs';
@import 'multicourse/media-kit';
@import 'multicourse/about_pages';
@import 'multicourse/press_release';
@import 'multicourse/password_reset';
@import 'multicourse/error-pages';
@import 'multicourse/help';
@import 'multicourse/edge';
## NOTE: needed here for cascade and dependency purposes, but not a great permanent solution
@import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
## 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
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'base/reset';
@import 'base/mixins';
@import 'base/variables';
## 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.get('THEME_NAME') is not None:
// 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';
// base - specific views
@import 'views/verification';
@import 'views/shoppingcart';
// applications
@import 'discussion';
@import 'news';
## NOTE: needed here for cascade and dependency purposes, but not a great permanent solution
@import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
## 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 // lms - css application architecture
// ==================== // ====================
// libs and resets *do not edit* // libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon @import 'bourbon/bourbon'; // lib - bourbon
// VENDOR + REBASE *referenced/used vendor presentation and reset*
// ====================
@import 'base/reset';
@import 'vendor/font-awesome';
@import 'vendor/responsive-carousel/responsive-carousel';
@import 'vendor/responsive-carousel/responsive-carousel.slide';
// BASE *default edX offerings* // BASE *default edX offerings*
// ==================== // ====================
// base - utilities
// base - utilities
@import 'base/reset';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/variables'; @import 'base/variables';
...@@ -46,10 +41,6 @@ ...@@ -46,10 +41,6 @@
@import 'elements/typography'; @import 'elements/typography';
@import 'elements/controls'; @import 'elements/controls';
// base - specific views
@import 'views/verification';
@import 'views/shoppingcart';
// shared - course // shared - course
@import 'shared/forms'; @import 'shared/forms';
@import 'shared/footer'; @import 'shared/footer';
...@@ -60,24 +51,5 @@ ...@@ -60,24 +51,5 @@
@import 'shared/activation_messages'; @import 'shared/activation_messages';
@import 'shared/unsubscribe'; @import 'shared/unsubscribe';
// shared - platform ## NOTE: needed here for cascade and dependency purposes, but not a great permanent solution
@import 'multicourse/home';
@import 'multicourse/dashboard';
@import 'multicourse/account';
@import 'multicourse/testcenter-register';
@import 'multicourse/courses';
@import 'multicourse/course_about';
@import 'multicourse/jobs';
@import 'multicourse/media-kit';
@import 'multicourse/about_pages';
@import 'multicourse/press_release';
@import 'multicourse/password_reset';
@import 'multicourse/error-pages';
@import 'multicourse/help';
@import 'multicourse/edge';
// applications
@import 'discussion';
@import 'news';
@import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/) @import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
...@@ -54,7 +54,6 @@ input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-s ...@@ -54,7 +54,6 @@ input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-s
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
textarea { overflow: auto; vertical-align: top; resize: vertical; } textarea { overflow: auto; vertical-align: top; resize: vertical; }
input:valid, textarea:valid { } input:valid, textarea:valid { }
input:invalid, textarea:invalid { background-color: #f0dddd; }
table { border-collapse: collapse; border-spacing: 0; } table { border-collapse: collapse; border-spacing: 0; }
td { vertical-align: top; } td { vertical-align: top; }
...@@ -70,7 +69,7 @@ td { vertical-align: top; } ...@@ -70,7 +69,7 @@ td { vertical-align: top; }
@media only screen and (min-width: 35em) { @media only screen and (min-width: 35em) {
} }
...@@ -85,13 +84,13 @@ td { vertical-align: top; } ...@@ -85,13 +84,13 @@ td { vertical-align: top; }
.clearfix { *zoom: 1; } .clearfix { *zoom: 1; }
@media print { @media print {
* { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; }
a, a:visited { text-decoration: underline; } a, a:visited { text-decoration: underline; }
a[href]:after { content: " (" attr(href) ")"; } a[href]:after { content: " (" attr(href) ")"; }
abbr[title]:after { content: " (" attr(title) ")"; } abbr[title]:after { content: " (" attr(title) ")"; }
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; }
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
thead { display: table-header-group; } thead { display: table-header-group; }
tr, img { page-break-inside: avoid; } tr, img { page-break-inside: avoid; }
img { max-width: 100% !important; } img { max-width: 100% !important; }
@page { margin: 0.5cm; } @page { margin: 0.5cm; }
......
...@@ -194,3 +194,92 @@ ...@@ -194,3 +194,92 @@
%t-weight5 { %t-weight5 {
font-weight: 700; font-weight: 700;
} }
// ====================
// MISC: extends - type
// application: canned headings
%hd-lv1 {
@extend %t-title1;
@extend %t-weight1;
color: $m-gray-d4;
margin: 0 0 ($baseline*2) 0;
}
%hd-lv2 {
@extend %t-title4;
@extend %t-weight1;
margin: 0 0 ($baseline*0.75) 0;
border-bottom: 1px solid $m-gray-l3;
padding-bottom: ($baseline/2);
color: $m-gray-d4;
}
%hd-lv3 {
@extend %t-title6;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
%hd-lv4 {
@extend %t-title6;
@extend %t-weight2;
margin: 0 0 $baseline 0;
color: $m-gray-d4;
}
%hd-lv5 {
@extend %t-title7;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
// application: canned copy
%copy-base {
@extend %t-copy-base;
color: $m-gray-d2;
}
%copy-lead1 {
@extend %t-copy-lead2;
color: $m-gray;
}
%copy-detail {
@extend %t-copy-sub1;
@extend %t-weight3;
color: $m-gray-d1;
}
%copy-metadata {
@extend %t-copy-sub2;
color: $m-gray-d1;
%copy-metadata-value {
@extend %t-weight2;
}
%copy-metadata-value {
@extend %t-weight4;
}
}
// application: canned links
%copy-link {
border-bottom: 1px dotted transparent;
&:hover, &:active {
border-color: $link-color-d1;
}
}
%copy-badge {
@extend %t-title8;
@extend %t-weight5;
border-radius: ($baseline/5);
padding: ($baseline/2) $baseline;
text-transform: uppercase;
}
...@@ -2,184 +2,194 @@ ...@@ -2,184 +2,194 @@
@import "base/variables"; @import "base/variables";
// These are all quick solutions for IE please rewrite // These are all quick solutions for IE please rewrite
//Make overlay white because ie doesn't like rgba .ie {
.highlighted-courses .courses .course header.course-preview, .find-courses .courses .course header.course-preview,
.home .highlighted-courses > h2, .home .highlighted-courses > section.outside-app h1, section.outside-app .home .highlighted-courses > h1,
header.global {
background: #FFF;
}
// hide all actions //Make overlay white because ie doesn't like rgba
.home > header .title .actions, .highlighted-courses .courses .course header.course-preview, .find-courses .courses .course header.course-preview,
.home > header .title:hover .actions { .home .highlighted-courses > h2, .home .highlighted-courses > section.outside-app h1, section.outside-app .home .highlighted-courses > h1,
display: none; header.global {
height: auto; background: #FFF;
} }
// hide all actions
.home > header .title .actions,
.home > header .title:hover .actions {
display: none;
height: auto;
}
.home > header .title { .home > header .title {
&:hover { &:hover {
> hgroup { > hgroup {
h1 { h1 {
border-bottom: 0; border-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
} }
h2 { h2 {
opacity: 1.0; opacity: 1.0;
}
} }
}
.actions { .actions {
opacity: 0; opacity: 0;
}
} }
} }
}
// because ie doesn't like :last // because ie doesn't like :last
.last { .last {
margin-right: 0 !important; margin-right: 0 !important;
}
// make partners not animate
.home .university-partners .partners a {
.name {
position: static;
} }
&:hover { // make partners not animate
text-decoration: none; .home .university-partners .partners a {
&::before {
opacity: 1.0;
}
.name { .name {
bottom: 0px; position: static;
} }
img { &:hover {
top: 0px; text-decoration: none;
}
}
} &::before {
opacity: 1.0;
}
.home .university-partners .partners { .name {
width: 660px; bottom: 0px;
}
li.partner { img {
float: left; top: 0px;
display: block; }
padding: 0; }
width: 220px;
overflow: hidden;
}
}
// make animations on homepage not animate and show everything
.highlighted-courses .courses .course, .find-courses .courses .course {
.meta-info {
display: none;
} }
.inner-wrapper { .home .university-partners .partners {
height: 100%; width: 660px;
overflow: visible;
position: relative; li.partner {
float: left;
display: block;
padding: 0;
width: 220px;
overflow: hidden;
}
} }
header.course-preview { // make animations on homepage not animate and show everything
left: 0px; .highlighted-courses .courses .course, .find-courses .courses .course {
position: relative; .meta-info {
top: 0px; display: none;
width: 100%; }
z-index: 3;
height: auto;
hgroup { .inner-wrapper {
height: 100%;
overflow: visible;
position: relative; position: relative;
right: 0;
top: 0;
} }
} header.course-preview {
left: 0px;
position: relative;
top: 0px;
width: 100%;
z-index: 3;
height: auto;
.info { hgroup {
height: auto; position: relative;
position: static; right: 0;
overflow: visible; top: 0;
}
.desc {
height: auto;
} }
}
&:hover {
background: rgb(245,245,245);
border-color: rgb(170,170,170);
box-shadow: 0 1px 16px 0 rgba($blue, 0.4);
.info { .info {
top: 0; height: auto;
position: static;
overflow: visible;
.desc {
height: auto;
}
} }
.meta-info { &:hover {
opacity: 0; background: rgb(245,245,245);
border-color: rgb(170,170,170);
box-shadow: 0 1px 16px 0 rgba($blue, 0.4);
.info {
top: 0;
}
.meta-info {
opacity: 0;
}
} }
} }
}
// make overlay flat black since IE cant handle rgba // make overlay flat black since IE cant handle rgba
#lean_overlay { #lean_overlay {
background: #000; background: #000;
} }
// active navigation // active navigation
nav.course-material ol.course-tabs li a.active, nav.course-material .xmodule_SequenceModule nav.sequence-nav ol.course-tabs li a.seq_video.active, .xmodule_SequenceModule nav.sequence-nav nav.course-material ol.course-tabs li a.seq_video.active { nav.course-material ol.course-tabs li a.active, nav.course-material .xmodule_SequenceModule nav.sequence-nav ol.course-tabs li a.seq_video.active, .xmodule_SequenceModule nav.sequence-nav nav.course-material ol.course-tabs li a.seq_video.active {
background-color: #333; background-color: #333;
background-color: rgba(0, 0, 0, .4); background-color: rgba(0, 0, 0, .4);
} }
// make dropdown user consistent size // make dropdown user consistent size
header.global ol.user > li.primary a.dropdown { header.global ol.user > li.primary a.dropdown {
padding-top: 6px; padding-top: 6px;
padding-bottom: 6px; padding-bottom: 6px;
} }
// always hide arrow in IE // always hide arrow in IE
.dashboard .my-courses .my-course .cover .arrow { .dashboard .my-courses .my-course .cover .arrow {
display: none; display: none;
} }
.ie-banner { div.course-wrapper {
display: block !important; display: block !important;
}
section.course-content,
section.course-index {
display: block !important;
float: left;
}
div.course-wrapper { section.course-content {
display: block !important; width: 71.27%;
}
}
section.course-content, .sidebar {
section.course-index { float: left !important;
display: block !important; display: block !important;
float: left;
} }
section.course-content { .sequence-nav ol {
width: 71.27%; display: block !important;
li {
float: left !important;
width: 50px;
}
} }
}
.sidebar { .course-wrapper {
float: left !important; clear: both !important;
display: block !important; }
} }
.sequence-nav ol { .lte9 {
display: block !important;
li { .ie-banner {
float: left !important; display: block !important;
width: 50px;
} }
} }
// lms - views - verification flow // lms - views - verification flow
// ==================== // ====================
// MISC: extends - type // MISC: extends - button
// application: canned headings
%hd-lv1 {
@extend %t-title1;
@extend %t-weight1;
color: $m-gray-d4;
margin: 0 0 ($baseline*2) 0;
}
%hd-lv2 {
@extend %t-title4;
@extend %t-weight1;
margin: 0 0 ($baseline*0.75) 0;
border-bottom: 1px solid $m-gray-l3;
padding-bottom: ($baseline/2);
color: $m-gray-d4;
}
%hd-lv3 {
@extend %t-title6;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
%hd-lv4 {
@extend %t-title6;
@extend %t-weight2;
margin: 0 0 $baseline 0;
color: $m-gray-d4;
}
%hd-lv5 {
@extend %t-title7;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
// application: canned copy
%copy-base {
@extend %t-copy-base;
color: $m-gray-d2;
}
%copy-lead1 {
@extend %t-copy-lead2;
color: $m-gray;
}
%copy-detail {
@extend %t-copy-sub1;
@extend %t-weight3;
color: $m-gray-d1;
}
%copy-metadata {
@extend %t-copy-sub2;
color: $m-gray-d1;
%copy-metadata-value {
@extend %t-weight2;
}
%copy-metadata-value {
@extend %t-weight4;
}
}
// application: canned links
%copy-link {
border-bottom: 1px dotted transparent;
&:hover, &:active {
border-color: $link-color-d1;
}
}
%copy-badge {
@extend %t-title8;
@extend %t-weight5;
border-radius: ($baseline/5);
padding: ($baseline/2) $baseline;
text-transform: uppercase;
}
// ====================
%btn-verify-primary { %btn-verify-primary {
@extend %btn-primary-green; @extend %btn-primary-green;
} }
......
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
<%block name="title"><title>${_("Courseware")} - ${settings.PLATFORM_NAME}</title></%block> <%block name="title"><title>${_("Courseware")} - ${settings.PLATFORM_NAME}</title></%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> <%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
<%block name="title"><title>${_("{course_number} Courseware").format(course_number=course.display_number_with_default) | h}</title></%block> <%block name="title"><title>${_("{course_number} Courseware").format(course_number=course.display_number_with_default) | h}</title></%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%include file="../discussion/_js_head_dependencies.html" /> <%static:css group='style-course'/>
<%include file="../discussion/_js_head_dependencies.html" />
% if show_chat: % if show_chat:
<link rel="stylesheet" href="${static.url('css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css')}" /> <link rel="stylesheet" href="${static.url('css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css')}" />
## It'd be better to have this in a place like lms/css/vendor/candy, ## It'd be better to have this in a place like lms/css/vendor/candy,
......
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
</%block> </%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<style type="text/css"> <style type="text/css">
% for (grade, _), color in zip(ordered_grades, ['green', 'Chocolate']): % for (grade, _), color in zip(ordered_grades, ['green', 'Chocolate']):
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%block name="title"><title>${_("{course.display_number_with_default} Course Info").format(course=course) | h}</title></%block> <%block name="title"><title>${_("{course.display_number_with_default} Course Info").format(course=course) | h}</title></%block>
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script>
......
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
<%block name="title"><title>${_("News - MITx 6.002x")}</title></%block> <%block name="title"><title>${_("News - MITx 6.002x")}</title></%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%namespace name="progress_graph" file="/courseware/progress_graph.js"/> <%namespace name="progress_graph" file="/courseware/progress_graph.js"/>
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%block name="title"><title>${course.display_number_with_default | h} ${tab['name']}</title></%block> <%block name="title"><title>${course.display_number_with_default | h} ${tab['name']}</title></%block>
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%block name="title"><title>${_("{course.display_number_with_default} Course Info").format(course=course) | h}</title></%block> <%block name="title"><title>${_("{course.display_number_with_default} Course Info").format(course=course) | h}</title></%block>
......
...@@ -9,8 +9,10 @@ ...@@ -9,8 +9,10 @@
<%block name="title"><title>${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block> <%block name="title"><title>${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%include file="_js_head_dependencies.html" /> <%static:css group='style-course'/>
<%include file="_js_head_dependencies.html" />
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
...@@ -28,7 +30,7 @@ ...@@ -28,7 +30,7 @@
<div class="discussion-column"> <div class="discussion-column">
</div> </div>
</div> </div>
</section> </section>
<%include file="_underscore_templates.html" /> <%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" /> <%include file="_thread_list_template.html" />
...@@ -10,7 +10,9 @@ ...@@ -10,7 +10,9 @@
<%block name="title"><title>${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block> <%block name="title"><title>${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<%include file="_js_head_dependencies.html" /> <%include file="_js_head_dependencies.html" />
</%block> </%block>
......
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
<%block name="title"><title>${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block> <%block name="title"><title>${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<%include file="_js_head_dependencies.html" /> <%include file="_js_head_dependencies.html" />
</%block> </%block>
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script>
...@@ -25,14 +27,14 @@ ...@@ -25,14 +27,14 @@
var i = 1 var i = 1
$(".hint-select").each(function(){ $(".hint-select").each(function(){
if ($(this).is(":checked")) { if ($(this).is(":checked")) {
data_dict[i] = [$(this).parent().attr("data-problem"), data_dict[i] = [$(this).parent().attr("data-problem"),
$(this).parent().attr("data-answer"), $(this).parent().attr("data-answer"),
$(this).parent().attr("data-pk")]; $(this).parent().attr("data-pk")];
i += 1 i += 1
} }
}); });
$.ajax(window.location.pathname, { $.ajax(window.location.pathname, {
type: "POST", type: "POST",
data: data_dict, data: data_dict,
success: update_contents success: update_contents
}); });
...@@ -42,16 +44,16 @@ ...@@ -42,16 +44,16 @@
var data_dict = {'op': 'change votes', var data_dict = {'op': 'change votes',
'field': field} 'field': field}
for (var i=0; i<changed_votes.length; i++) { for (var i=0; i<changed_votes.length; i++) {
data_dict[i] = [$(changed_votes[i]).parent().attr("data-problem"), data_dict[i] = [$(changed_votes[i]).parent().attr("data-problem"),
$(changed_votes[i]).parent().attr("data-answer"), $(changed_votes[i]).parent().attr("data-answer"),
$(changed_votes[i]).parent().attr("data-pk"), $(changed_votes[i]).parent().attr("data-pk"),
$(changed_votes[i]).val()]; $(changed_votes[i]).val()];
} }
$.ajax(window.location.pathname, { $.ajax(window.location.pathname, {
type: "POST", type: "POST",
data: data_dict, data: data_dict,
success: update_contents success: update_contents
}); });
}); });
$("#switch-fields").click(function(){ $("#switch-fields").click(function(){
...@@ -61,7 +63,7 @@ ...@@ -61,7 +63,7 @@
type: "POST", type: "POST",
data: out_dict, data: out_dict,
success: update_contents success: update_contents
}); });
}); });
...@@ -87,14 +89,14 @@ ...@@ -87,14 +89,14 @@
var i = 1 var i = 1
$(".hint-select").each(function(){ $(".hint-select").each(function(){
if ($(this).is(":checked")) { if ($(this).is(":checked")) {
data_dict[i] = [$(this).parent().attr("data-problem"), data_dict[i] = [$(this).parent().attr("data-problem"),
$(this).parent().attr("data-answer"), $(this).parent().attr("data-answer"),
$(this).parent().attr("data-pk")]; $(this).parent().attr("data-pk")];
i += 1 i += 1
} }
}); });
$.ajax(window.location.pathname, { $.ajax(window.location.pathname, {
type: "POST", type: "POST",
data: data_dict, data: data_dict,
success: update_contents success: update_contents
}); });
...@@ -102,7 +104,7 @@ ...@@ -102,7 +104,7 @@
} }
$(document).ready(setup); $(document).ready(setup);
function update_contents(data, status, jqXHR) { function update_contents(data, status, jqXHR) {
$('.instructor-dashboard-content').html(data.contents); $('.instructor-dashboard-content').html(data.contents);
setup(); setup();
......
...@@ -17,7 +17,8 @@ ...@@ -17,7 +17,8 @@
## 6. And tests go in lms/djangoapps/instructor/tests/ ## 6. And tests go in lms/djangoapps/instructor/tests/
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%block name="title"><title>${_("{course_number} Staff Grading").format(course_number=course.display_number_with_default) | h}</title></%block> <%block name="title"><title>${_("{course_number} Staff Grading").format(course_number=course.display_number_with_default) | h}</title></%block>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="main.html" /> <%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
......
...@@ -16,9 +16,10 @@ ...@@ -16,9 +16,10 @@
</%def> </%def>
<!DOCTYPE html> <!DOCTYPE html>
<!--[if lt IE 8]><html class="ie"><![endif]--> <!--[if IE 7]><html class="ie ie7 lte9 lte8 lte7"><![endif]-->
<!--[if IE 8]><html class="ie8"><![endif]--> <!--[if IE 8]><html class="ie ie8 lte9 lte8"><![endif]-->
<!--[if gte IE 9]><!--><html><!--<![endif]--> <!--[if IE 9]><html class="ie ie9 lte9"><![endif]-->
<!--[if gt IE 9]><!--><html><!--<![endif]-->
<head> <head>
<%block name="title"> <%block name="title">
% if stanford_theme_enabled(): % if stanford_theme_enabled():
...@@ -45,21 +46,22 @@ ...@@ -45,21 +46,22 @@
<link rel="icon" type="image/x-icon" href="${static.url(settings.FAVICON_PATH)}" /> <link rel="icon" type="image/x-icon" href="${static.url(settings.FAVICON_PATH)}" />
<%static:css group='application'/> <%static:css group='style-vendor'/>
<%static:css group='style-app'/>
<%static:css group='style-app-extend1'/>
<%static:css group='style-app-extend2'/>
<%static:js group='main_vendor'/> <%static:js group='main_vendor'/>
<%block name="headextra"/> <%block name="headextra"/>
% if theme_enabled(): % if theme_enabled():
<%include file="theme-head-extra.html" /> <%include file="theme-head-extra.html" />
% endif % endif
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="${static.url('js/html5shiv.js')}"></script> <script src="${static.url('js/html5shiv.js')}"></script>
<![endif]--> <![endif]-->
<!--[if lte IE 9]>
<%static:css group='ie-fixes'/>
<![endif]-->
<meta name="path_prefix" content="${MITX_ROOT_URL}"> <meta name="path_prefix" content="${MITX_ROOT_URL}">
<meta name="google-site-verification" content="_mipQ4AtZQDNmbtOkwehQDOgCxUUV2fb_C0b6wbiRHY" /> <meta name="google-site-verification" content="_mipQ4AtZQDNmbtOkwehQDOgCxUUV2fb_C0b6wbiRHY" />
......
...@@ -7,7 +7,13 @@ ...@@ -7,7 +7,13 @@
<link rel="icon" type="image/x-icon" href="{% static "images/favicon.ico" %}" /> <link rel="icon" type="image/x-icon" href="{% static "images/favicon.ico" %}" />
{% compressed_css 'application' %} {% compressed_css 'style-vendor' %}
{% compressed_css 'style-app' %}
{% compressed_css 'style-app-extend1' %}
{% compressed_css 'style-app-extend2' %}
{% compressed_css 'style-course-vendor' %}
{% compressed_css 'style-course' %}
{% block main_vendor_js %} {% block main_vendor_js %}
{% compressed_js 'main_vendor' %} {% compressed_js 'main_vendor' %}
{% endblock %} {% endblock %}
...@@ -19,9 +25,6 @@ ...@@ -19,9 +25,6 @@
<script src="${static.url('js/html5shiv.js')}"></script> <script src="${static.url('js/html5shiv.js')}"></script>
<![endif]--> <![endif]-->
<!--[if lte IE 9]>
<%static:css group='ie-fixes'/>
<![endif]-->
<meta name="path_prefix" content="{{MITX_ROOT_URL}}"> <meta name="path_prefix" content="{{MITX_ROOT_URL}}">
</head> </head>
......
<%inherit file="main.html" />
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<h2>${_("Disable or Reenable student accounts")}</h2>
<form action="${reverse('disable_account_ajax')}" method="post" data-remote="true" id="disable-form">
<label for="username">${_("Username:")}</label>
<input type="text" id="username" name="username" required="true">
<br>
<label for="account_action">${_("Disable Account")}</label>
<input type="radio" name="account_action" value="disable" id="account_action">
<br>
<label for="account_action">${_("Reenable Account")}</label>
<input type="radio" name="account_action" value="reenable" id="account_action">
<br>
<br>
</form>
<button id="submit-form">${_("Submit")}</button>
<br>
<br>
<p id="account-change-status"></p>
<h2>${_("Students whose accounts have been disabled")}</h2>
<p>${_("(reload your page to refresh)")}</p>
<table id="account-table" border='1'>
<tr>
% for header in headers:
<th>${header}</th>
% endfor
</tr>
% for row in rows:
<tr>
% for cell in row:
<td>${cell}</td>
% endfor
</tr>
% endfor
</table>
<script type="text/javascript">
$(function() {
var form = $("#disable-form");
$("#submit-form").click(function(){
$("#account-change-status").html(gettext("working..."));
$.ajax({
type: "POST",
url: form.attr('action'),
data: form.serialize(),
success: function(response){
$("#account-change-status").html(response.message);
},
});
});
});
</script>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<!DOCTYPE html> <!DOCTYPE html>
<html class="view-iframe"> <!--[if IE 7]><html class="ie ie7 lte9 lte8 lte7 view-iframe"><![endif]-->
<!--[if IE 8]><html class="ie ie8 lte9 lte8 view-iframe"><![endif]-->
<!--[if IE 9]><html class="ie ie9 lte9 view-iframe"><![endif]-->
<!--[if gt IE 9]><!--><html class="view-iframe"><!--<![endif]-->
<head> <head>
<%block name="title"></%block> <%block name="title"></%block>
...@@ -8,17 +11,15 @@ ...@@ -8,17 +11,15 @@
<meta name="path_prefix" content="${MITX_ROOT_URL}" /> <meta name="path_prefix" content="${MITX_ROOT_URL}" />
<meta name="google-site-verification" content="_mipQ4AtZQDNmbtOkwehQDOgCxUUV2fb_C0b6wbiRHY" /> <meta name="google-site-verification" content="_mipQ4AtZQDNmbtOkwehQDOgCxUUV2fb_C0b6wbiRHY" />
<%static:css group='application'/> <%static:css group='style-vendor'/>
<%static:css group='style-app'/>
<%static:js group='main_vendor'/> <%static:js group='main_vendor'/>
<!--[if IE]> <!--[if lt IE 9]>
<script src="${static.url('js/html5shiv.js')}"></script> <script src="${static.url('js/html5shiv.js')}"></script>
<![endif]--> <![endif]-->
<!--[if lte IE 9]>
<%static:css group='ie-fixes'/>
<![endif]-->
<%block name="headextra"/> <%block name="headextra"/>
% if not course: % if not course:
......
...@@ -7,8 +7,10 @@ ...@@ -7,8 +7,10 @@
%> %>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:js group='courseware'/> <%static:css group='style-course'/>
<%static:js group='courseware'/>
<style type="text/css"> <style type="text/css">
blockquote { blockquote {
background:#f9f9f9; background:#f9f9f9;
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block> </%block>
<%block name="title"><title>${_("{course_number} Combined Notifications").format(course_number=course.display_number_with_default) | h}</title></%block> <%block name="title"><title>${_("{course_number} Combined Notifications").format(course_number=course.display_number_with_default) | h}</title></%block>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment