@@ -114,7 +113,7 @@ from contentstore import utils
A brief description of your education, experience, and expertise
-
+
@@ -143,7 +142,7 @@ from contentstore import utils
-
+
randomize all problems
@@ -217,7 +216,7 @@ from contentstore import utils
-
+
randomize all problems
@@ -283,7 +282,7 @@ from contentstore import utils
Discussions
-
+
General Settings
@@ -296,7 +295,7 @@ from contentstore import utils
-
+
Students and faculty will be able to post anonymously
@@ -320,7 +319,7 @@ from contentstore import utils
-
+
Students and faculty will be able to post anonymously
@@ -329,7 +328,7 @@ from contentstore import utils
-
+
This option is disabled since there are previous discussions that are anonymous.
@@ -351,7 +350,7 @@ from contentstore import utils
-
+
diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html
index ceab8cd862..2c6846bece 100644
--- a/cms/templates/settings_graders.html
+++ b/cms/templates/settings_graders.html
@@ -12,7 +12,6 @@ from contentstore import utils
-
diff --git a/common/djangoapps/django_comment_common/__init__.py b/common/djangoapps/django_comment_common/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/djangoapps/django_comment_common/migrations/0001_initial.py b/common/djangoapps/django_comment_common/migrations/0001_initial.py
new file mode 100644
index 0000000000..f2c3ca3aee
--- /dev/null
+++ b/common/djangoapps/django_comment_common/migrations/0001_initial.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+from south.v2 import SchemaMigration
+
+
+class Migration(SchemaMigration):
+#
+# cdodge: This is basically an empty migration since everything has - up to now - managed in the django_comment_client app
+# But going forward we should be using this migration
+#
+ def forwards(self, orm):
+ pass
+
+ def backwards(self, orm):
+ pass
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'django_comment_common.permission': {
+ 'Meta': {'object_name': 'Permission'},
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}),
+ 'roles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['django_comment_common.Role']"})
+ },
+ 'django_comment_common.role': {
+ 'Meta': {'object_name': 'Role'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'symmetrical': 'False', 'to': "orm['auth.User']"})
+ }
+ }
+
+ complete_apps = ['django_comment_common']
diff --git a/common/djangoapps/django_comment_common/migrations/__init__.py b/common/djangoapps/django_comment_common/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/djangoapps/django_comment_common/models.py b/common/djangoapps/django_comment_common/models.py
new file mode 100644
index 0000000000..ec722b718a
--- /dev/null
+++ b/common/djangoapps/django_comment_common/models.py
@@ -0,0 +1,74 @@
+import logging
+
+from django.db import models
+from django.contrib.auth.models import User
+
+from django.dispatch import receiver
+from django.db.models.signals import post_save
+
+from student.models import CourseEnrollment
+
+from xmodule.modulestore.django import modulestore
+from xmodule.course_module import CourseDescriptor
+
+FORUM_ROLE_ADMINISTRATOR = 'Administrator'
+FORUM_ROLE_MODERATOR = 'Moderator'
+FORUM_ROLE_COMMUNITY_TA = 'Community TA'
+FORUM_ROLE_STUDENT = 'Student'
+
+
+@receiver(post_save, sender=CourseEnrollment)
+def assign_default_role(sender, instance, **kwargs):
+ if instance.user.is_staff:
+ role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
+ else:
+ role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
+
+ logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
+ instance.user.roles.add(role)
+
+
+class Role(models.Model):
+ name = models.CharField(max_length=30, null=False, blank=False)
+ users = models.ManyToManyField(User, related_name="roles")
+ course_id = models.CharField(max_length=255, blank=True, db_index=True)
+
+ class Meta:
+ # use existing table that was originally created from django_comment_client app
+ db_table = 'django_comment_client_role'
+
+ def __unicode__(self):
+ return self.name + " for " + (self.course_id if self.course_id else "all courses")
+
+ def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
+ # since it's one-off and doesn't handle inheritance later
+ if role.course_id and role.course_id != self.course_id:
+ logging.warning("%s cannot inherit permissions from %s due to course_id inconsistency", \
+ self, role)
+ for per in role.permissions.all():
+ self.add_permission(per)
+
+ def add_permission(self, permission):
+ self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
+
+ def has_permission(self, permission):
+ course_loc = CourseDescriptor.id_to_location(self.course_id)
+ course = modulestore().get_instance(self.course_id, course_loc)
+ if self.name == FORUM_ROLE_STUDENT and \
+ (permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
+ (not course.forum_posts_allowed):
+ return False
+
+ return self.permissions.filter(name=permission).exists()
+
+
+class Permission(models.Model):
+ name = models.CharField(max_length=30, null=False, blank=False, primary_key=True)
+ roles = models.ManyToManyField(Role, related_name="permissions")
+
+ class Meta:
+ # use existing table that was originally created from django_comment_client app
+ db_table = 'django_comment_client_permission'
+
+ def __unicode__(self):
+ return self.name
diff --git a/common/djangoapps/django_comment_common/utils.py b/common/djangoapps/django_comment_common/utils.py
new file mode 100644
index 0000000000..f74116d59f
--- /dev/null
+++ b/common/djangoapps/django_comment_common/utils.py
@@ -0,0 +1,56 @@
+from django_comment_common.models import Role
+
+_STUDENT_ROLE_PERMISSIONS = ["vote", "update_thread", "follow_thread", "unfollow_thread",
+ "update_comment", "create_sub_comment", "unvote", "create_thread",
+ "follow_commentable", "unfollow_commentable", "create_comment", ]
+
+_MODERATOR_ROLE_PERMISSIONS = ["edit_content", "delete_thread", "openclose_thread",
+ "endorse_comment", "delete_comment", "see_all_cohorts"]
+
+_ADMINISTRATOR_ROLE_PERMISSIONS = ["manage_moderator"]
+
+def seed_permissions_roles(course_id):
+ administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
+ moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
+ community_ta_role = Role.objects.get_or_create(name="Community TA", course_id=course_id)[0]
+ student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
+
+ for per in _STUDENT_ROLE_PERMISSIONS:
+ student_role.add_permission(per)
+
+ for per in _MODERATOR_ROLE_PERMISSIONS:
+ moderator_role.add_permission(per)
+
+ for per in _ADMINISTRATOR_ROLE_PERMISSIONS:
+ administrator_role.add_permission(per)
+
+ moderator_role.inherit_permissions(student_role)
+
+ # For now, Community TA == Moderator, except for the styling.
+ community_ta_role.inherit_permissions(moderator_role)
+
+ administrator_role.inherit_permissions(moderator_role)
+
+
+def are_permissions_roles_seeded(course_id):
+
+ try:
+ administrator_role = Role.objects.get(name="Administrator", course_id=course_id)
+ moderator_role = Role.objects.get(name="Moderator", course_id=course_id)
+ student_role = Role.objects.get(name="Student", course_id=course_id)
+ except:
+ return False
+
+ for per in _STUDENT_ROLE_PERMISSIONS:
+ if not student_role.has_permission(per):
+ return False
+
+ for per in _MODERATOR_ROLE_PERMISSIONS + _STUDENT_ROLE_PERMISSIONS:
+ if not moderator_role.has_permission(per):
+ return False
+
+ for per in _ADMINISTRATOR_ROLE_PERMISSIONS + _MODERATOR_ROLE_PERMISSIONS + _STUDENT_ROLE_PERMISSIONS:
+ if not administrator_role.has_permission(per):
+ return False
+
+ return True
diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py
index 9560025441..d73bb6f01d 100644
--- a/common/djangoapps/student/tests/factories.py
+++ b/common/djangoapps/student/tests/factories.py
@@ -1,43 +1,47 @@
from student.models import (User, UserProfile, Registration,
- CourseEnrollmentAllowed, CourseEnrollment)
+ CourseEnrollmentAllowed, CourseEnrollment,
+ PendingEmailChange)
from django.contrib.auth.models import Group
from datetime import datetime
-from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall, post_generation
+from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
from uuid import uuid4
+# Factories don't have __init__ methods, and are self documenting
+# pylint: disable=W0232
+
class GroupFactory(DjangoModelFactory):
FACTORY_FOR = Group
- name = 'staff_MITx/999/Robot_Super_Course'
+ name = u'staff_MITx/999/Robot_Super_Course'
class UserProfileFactory(DjangoModelFactory):
FACTORY_FOR = UserProfile
user = None
- name = 'Robot Test'
+ name = u'Robot Test'
level_of_education = None
- gender = 'm'
+ gender = u'm'
mailing_address = None
- goals = 'World domination'
+ goals = u'World domination'
class RegistrationFactory(DjangoModelFactory):
FACTORY_FOR = Registration
user = None
- activation_key = uuid4().hex
+ activation_key = uuid4().hex.decode('ascii')
class UserFactory(DjangoModelFactory):
FACTORY_FOR = User
- username = 'robot'
- email = 'robot+test@edx.org'
+ username = Sequence(u'robot{0}'.format)
+ email = Sequence(u'robot+test+{0}@edx.org'.format)
password = PostGenerationMethodCall('set_password',
'test')
- first_name = 'Robot'
+ first_name = Sequence(u'Robot{0}'.format)
last_name = 'Test'
is_staff = False
is_active = True
@@ -64,7 +68,7 @@ class CourseEnrollmentFactory(DjangoModelFactory):
FACTORY_FOR = CourseEnrollment
user = SubFactory(UserFactory)
- course_id = 'edX/toy/2012_Fall'
+ course_id = u'edX/toy/2012_Fall'
class CourseEnrollmentAllowedFactory(DjangoModelFactory):
@@ -72,3 +76,17 @@ class CourseEnrollmentAllowedFactory(DjangoModelFactory):
email = 'test@edx.org'
course_id = 'edX/test/2012_Fall'
+
+
+class PendingEmailChangeFactory(DjangoModelFactory):
+ """Factory for PendingEmailChange objects
+
+ user: generated by UserFactory
+ new_email: sequence of new+email+{}@edx.org
+ activation_key: sequence of integers, padded to 30 characters
+ """
+ FACTORY_FOR = PendingEmailChange
+
+ user = SubFactory(UserFactory)
+ new_email = Sequence(u'new+email+{0}@edx.org'.format)
+ activation_key = Sequence(u'{:0<30d}'.format)
diff --git a/common/djangoapps/student/tests/test_email.py b/common/djangoapps/student/tests/test_email.py
new file mode 100644
index 0000000000..3b31bb5c28
--- /dev/null
+++ b/common/djangoapps/student/tests/test_email.py
@@ -0,0 +1,261 @@
+import json
+import django.db
+
+from student.tests.factories import UserFactory, RegistrationFactory, PendingEmailChangeFactory
+from student.views import reactivation_email_for_user, change_email_request, confirm_email_change
+from student.models import UserProfile, PendingEmailChange
+from django.contrib.auth.models import User
+from django.test import TestCase, TransactionTestCase
+from django.test.client import RequestFactory
+from mock import Mock, patch
+from django.http import Http404, HttpResponse
+from django.conf import settings
+from nose.plugins.skip import SkipTest
+
+
+class TestException(Exception):
+ """Exception used for testing that nothing will catch explicitly"""
+ pass
+
+
+def mock_render_to_string(template_name, context):
+ """Return a string that encodes template_name and context"""
+ return str((template_name, sorted(context.iteritems())))
+
+
+def mock_render_to_response(template_name, context):
+ """Return an HttpResponse with content that encodes template_name and context"""
+ return HttpResponse(mock_render_to_string(template_name, context))
+
+
+class EmailTestMixin(object):
+ """Adds useful assertions for testing `email_user`"""
+
+ def assertEmailUser(self, email_user, subject_template, subject_context, body_template, body_context):
+ """Assert that `email_user` was used to send and email with the supplied subject and body
+
+ `email_user`: The mock `django.contrib.auth.models.User.email_user` function
+ to verify
+ `subject_template`: The template to have been used for the subject
+ `subject_context`: The context to have been used for the subject
+ `body_template`: The template to have been used for the body
+ `body_context`: The context to have been used for the body
+ """
+ email_user.assert_called_with(
+ mock_render_to_string(subject_template, subject_context),
+ mock_render_to_string(body_template, body_context),
+ settings.DEFAULT_FROM_EMAIL
+ )
+
+
+@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
+@patch('django.contrib.auth.models.User.email_user')
+class ReactivationEmailTests(EmailTestMixin, TestCase):
+ """Test sending a reactivation email to a user"""
+
+ def setUp(self):
+ self.user = UserFactory.create()
+ self.registration = RegistrationFactory.create(user=self.user)
+
+ def reactivation_email(self):
+ """Send the reactivation email, and return the response as json data"""
+ return json.loads(reactivation_email_for_user(self.user).content)
+
+ def assertReactivateEmailSent(self, email_user):
+ """Assert that the correct reactivation email has been sent"""
+ context = {
+ 'name': self.user.profile.name,
+ 'key': self.registration.activation_key
+ }
+
+ self.assertEmailUser(
+ email_user,
+ 'emails/activation_email_subject.txt',
+ context,
+ 'emails/activation_email.txt',
+ context
+ )
+
+ def test_reactivation_email_failure(self, email_user):
+ self.user.email_user.side_effect = Exception
+ response_data = self.reactivation_email()
+
+ self.assertReactivateEmailSent(email_user)
+ self.assertFalse(response_data['success'])
+
+ def test_reactivation_email_success(self, email_user):
+ response_data = self.reactivation_email()
+
+ self.assertReactivateEmailSent(email_user)
+ self.assertTrue(response_data['success'])
+
+
+class EmailChangeRequestTests(TestCase):
+ """Test changing a user's email address"""
+
+ def setUp(self):
+ self.user = UserFactory.create()
+ self.new_email = 'new.email@edx.org'
+ self.req_factory = RequestFactory()
+ self.request = self.req_factory.post('unused_url', data={
+ 'password': 'test',
+ 'new_email': self.new_email
+ })
+ self.request.user = self.user
+ self.user.email_user = Mock()
+
+ def run_request(self, request=None):
+ """Execute request and return result parsed as json
+
+ If request isn't passed in, use self.request instead
+ """
+ if request is None:
+ request = self.request
+
+ response = change_email_request(self.request)
+ return json.loads(response.content)
+
+ def assertFailedRequest(self, response_data, expected_error):
+ """Assert that `response_data` indicates a failed request that returns `expected_error`"""
+ self.assertFalse(response_data['success'])
+ self.assertEquals(expected_error, response_data['error'])
+ self.assertFalse(self.user.email_user.called)
+
+ def test_unauthenticated(self):
+ self.user.is_authenticated = False
+ with self.assertRaises(Http404):
+ change_email_request(self.request)
+ self.assertFalse(self.user.email_user.called)
+
+ def test_invalid_password(self):
+ self.request.POST['password'] = 'wrong'
+ self.assertFailedRequest(self.run_request(), 'Invalid password')
+
+ def test_invalid_emails(self):
+ for email in ('bad_email', 'bad_email@', '@bad_email'):
+ self.request.POST['new_email'] = email
+ self.assertFailedRequest(self.run_request(), 'Valid e-mail address required.')
+
+ def check_duplicate_email(self, email):
+ """Test that a request to change a users email to `email` fails"""
+ request = self.req_factory.post('unused_url', data={
+ 'new_email': email,
+ 'password': 'test',
+ })
+ request.user = self.user
+ self.assertFailedRequest(self.run_request(request), 'An account with this e-mail already exists.')
+
+ def test_duplicate_email(self):
+ UserFactory.create(email=self.new_email)
+ self.check_duplicate_email(self.new_email)
+
+ def test_capitalized_duplicate_email(self):
+ raise SkipTest("We currently don't check for emails in a case insensitive way, but we should")
+ UserFactory.create(email=self.new_email)
+ self.check_duplicate_email(self.new_email.capitalize())
+
+ # TODO: Finish testing the rest of change_email_request
+
+
+@patch('django.contrib.auth.models.User.email_user')
+@patch('student.views.render_to_response', Mock(side_effect=mock_render_to_response, autospec=True))
+@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
+class EmailChangeConfirmationTests(EmailTestMixin, TransactionTestCase):
+ """Test that confirmation of email change requests function even in the face of exceptions thrown while sending email"""
+ def setUp(self):
+ self.user = UserFactory.create()
+ self.profile = UserProfile.objects.get(user=self.user)
+ self.req_factory = RequestFactory()
+ self.request = self.req_factory.get('unused_url')
+ self.request.user = self.user
+ self.user.email_user = Mock()
+ self.pending_change_request = PendingEmailChangeFactory.create(user=self.user)
+ self.key = self.pending_change_request.activation_key
+
+ def assertRolledBack(self):
+ """Assert that no changes to user, profile, or pending email have been made to the db"""
+ self.assertEquals(self.user.email, User.objects.get(username=self.user.username).email)
+ self.assertEquals(self.profile.meta, UserProfile.objects.get(user=self.user).meta)
+ self.assertEquals(1, PendingEmailChange.objects.count())
+
+ def assertFailedBeforeEmailing(self, email_user):
+ """Assert that the function failed before emailing a user"""
+ self.assertRolledBack()
+ self.assertFalse(email_user.called)
+
+ def check_confirm_email_change(self, expected_template, expected_context):
+ """Call `confirm_email_change` and assert that the content was generated as expected
+
+ `expected_template`: The name of the template that should have been used
+ to generate the content
+ `expected_context`: The context dictionary that should have been used to
+ generate the content
+ """
+ response = confirm_email_change(self.request, self.key)
+ self.assertEquals(
+ mock_render_to_response(expected_template, expected_context).content,
+ response.content
+ )
+
+ def assertChangeEmailSent(self, email_user):
+ """Assert that the correct email was sent to confirm an email change"""
+ context = {
+ 'old_email': self.user.email,
+ 'new_email': self.pending_change_request.new_email,
+ }
+ self.assertEmailUser(
+ email_user,
+ 'emails/email_change_subject.txt',
+ context,
+ 'emails/confirm_email_change.txt',
+ context
+ )
+
+ def test_not_pending(self, email_user):
+ self.key = 'not_a_key'
+ self.check_confirm_email_change('invalid_email_key.html', {})
+ self.assertFailedBeforeEmailing(email_user)
+
+ def test_duplicate_email(self, email_user):
+ UserFactory.create(email=self.pending_change_request.new_email)
+ self.check_confirm_email_change('email_exists.html', {})
+ self.assertFailedBeforeEmailing(email_user)
+
+ def test_old_email_fails(self, email_user):
+ email_user.side_effect = [Exception, None]
+ self.check_confirm_email_change('email_change_failed.html', {
+ 'email': self.user.email,
+ })
+ self.assertRolledBack()
+ self.assertChangeEmailSent(email_user)
+
+ def test_new_email_fails(self, email_user):
+ email_user.side_effect = [None, Exception]
+ self.check_confirm_email_change('email_change_failed.html', {
+ 'email': self.pending_change_request.new_email
+ })
+ self.assertRolledBack()
+ self.assertChangeEmailSent(email_user)
+
+ def test_successful_email_change(self, email_user):
+ self.check_confirm_email_change('email_change_successful.html', {
+ 'old_email': self.user.email,
+ 'new_email': self.pending_change_request.new_email
+ })
+ self.assertChangeEmailSent(email_user)
+ meta = json.loads(UserProfile.objects.get(user=self.user).meta)
+ self.assertIn('old_emails', meta)
+ self.assertEquals(self.user.email, meta['old_emails'][0][0])
+ self.assertEquals(
+ self.pending_change_request.new_email,
+ User.objects.get(username=self.user.username).email
+ )
+ self.assertEquals(0, PendingEmailChange.objects.count())
+
+ @patch('student.views.PendingEmailChange.objects.get', Mock(side_effect=TestException))
+ @patch('student.views.transaction.rollback', wraps=django.db.transaction.rollback)
+ def test_always_rollback(self, rollback, _email_user):
+ with self.assertRaises(TestException):
+ confirm_email_change(self.request, self.key)
+
+ rollback.assert_called_with()
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index e8a70d6089..8059026e12 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -19,7 +19,7 @@ from django.core.context_processors import csrf
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.validators import validate_email, validate_slug, ValidationError
-from django.db import IntegrityError
+from django.db import IntegrityError, transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect, Http404
from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie, csrf_exempt
@@ -655,7 +655,7 @@ def create_account(request, post_override=None):
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except:
- log.exception(sys.exc_info())
+ log.warning('Unable to send activation email to user', exc_info=True)
js['value'] = 'Could not send activation e-mail.'
return HttpResponse(json.dumps(js))
@@ -975,7 +975,11 @@ def reactivation_email_for_user(user):
subject = ''.join(subject.splitlines())
message = render_to_string('emails/activation_email.txt', d)
- res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
+ try:
+ res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
+ except:
+ log.warning('Unable to send reactivation email', exc_info=True)
+ return HttpResponse(json.dumps({'success': False, 'error': 'Unable to send reactivation email'}))
return HttpResponse(json.dumps({'success': True}))
@@ -1001,7 +1005,7 @@ def change_email_request(request):
return HttpResponse(json.dumps({'success': False,
'error': 'Valid e-mail address required.'}))
- if len(User.objects.filter(email=new_email)) != 0:
+ if User.objects.filter(email=new_email).count() != 0:
## CRITICAL TODO: Handle case sensitivity for e-mails
return HttpResponse(json.dumps({'success': False,
'error': 'An account with this e-mail already exists.'}))
@@ -1036,41 +1040,63 @@ def change_email_request(request):
@ensure_csrf_cookie
+@transaction.commit_manually
def confirm_email_change(request, key):
''' User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update
'''
try:
- pec = PendingEmailChange.objects.get(activation_key=key)
- except PendingEmailChange.DoesNotExist:
- return render_to_response("invalid_email_key.html", {})
+ try:
+ pec = PendingEmailChange.objects.get(activation_key=key)
+ except PendingEmailChange.DoesNotExist:
+ transaction.rollback()
+ return render_to_response("invalid_email_key.html", {})
- user = pec.user
- d = {'old_email': user.email,
- 'new_email': pec.new_email}
+ user = pec.user
+ address_context = {
+ 'old_email': user.email,
+ 'new_email': pec.new_email
+ }
- if len(User.objects.filter(email=pec.new_email)) != 0:
- return render_to_response("email_exists.html", d)
+ if len(User.objects.filter(email=pec.new_email)) != 0:
+ transaction.rollback()
+ return render_to_response("email_exists.html", {})
- subject = render_to_string('emails/email_change_subject.txt', d)
- subject = ''.join(subject.splitlines())
- message = render_to_string('emails/confirm_email_change.txt', d)
- up = UserProfile.objects.get(user=user)
- meta = up.get_meta()
- if 'old_emails' not in meta:
- meta['old_emails'] = []
- meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()])
- up.set_meta(meta)
- up.save()
- # Send it to the old email...
- user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
- user.email = pec.new_email
- user.save()
- pec.delete()
- # And send it to the new email...
- user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
+ subject = render_to_string('emails/email_change_subject.txt', address_context)
+ subject = ''.join(subject.splitlines())
+ message = render_to_string('emails/confirm_email_change.txt', address_context)
+ up = UserProfile.objects.get(user=user)
+ meta = up.get_meta()
+ if 'old_emails' not in meta:
+ meta['old_emails'] = []
+ meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()])
+ up.set_meta(meta)
+ up.save()
+ # Send it to the old email...
+ try:
+ user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
+ except Exception:
+ transaction.rollback()
+ log.warning('Unable to send confirmation email to old address', exc_info=True)
+ return render_to_response("email_change_failed.html", {'email': user.email})
- return render_to_response("email_change_successful.html", d)
+ user.email = pec.new_email
+ user.save()
+ pec.delete()
+ # And send it to the new email...
+ try:
+ user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
+ except Exception:
+ transaction.rollback()
+ log.warning('Unable to send confirmation email to new address', exc_info=True)
+ return render_to_response("email_change_failed.html", {'email': pec.new_email})
+
+ transaction.commit()
+ return render_to_response("email_change_successful.html", address_context)
+ except Exception:
+ # If we get an unexpected exception, be sure to rollback the transaction
+ transaction.rollback()
+ raise
@ensure_csrf_cookie
diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py
index 40b839ae24..79e9b0afdb 100644
--- a/common/djangoapps/terrain/ui_helpers.py
+++ b/common/djangoapps/terrain/ui_helpers.py
@@ -123,3 +123,17 @@ def save_the_html(path='/tmp'):
f = open('%s/%s' % (path, filename), 'w')
f.write(html)
f.close()
+
+
+@world.absorb
+def click_course_settings():
+ course_settings_css = 'li.nav-course-settings'
+ if world.browser.is_element_present_by_css(course_settings_css):
+ world.css_click(course_settings_css)
+
+
+@world.absorb
+def click_tools():
+ tools_css = 'li.nav-course-tools'
+ if world.browser.is_element_present_by_css(tools_css):
+ world.css_click(tools_css)
diff --git a/common/lib/capa/jasmine_test_runner.html.erb b/common/lib/capa/jasmine_test_runner.html.erb
deleted file mode 100644
index 7b078daedd..0000000000
--- a/common/lib/capa/jasmine_test_runner.html.erb
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
- Jasmine Test Runner
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <% for src in js_source %>
-
- <% end %>
-
-
- <% for src in js_specs %>
-
- <% end %>
-
-
-
-
-
-
-
-
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index ae04e3aac4..33c7b61251 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -9,7 +9,7 @@ import re
from collections import namedtuple
from .exceptions import InvalidLocationError, InsufficientSpecificationError
-from xmodule.errortracker import ErrorLog, make_error_tracker
+from xmodule.errortracker import make_error_tracker
from bson.son import SON
log = logging.getLogger('mitx.' + 'modulestore')
@@ -64,7 +64,6 @@ class Location(_LocationBase):
"""
return re.sub('_+', '_', invalid.sub('_', value))
-
@staticmethod
def clean(value):
"""
@@ -72,7 +71,6 @@ class Location(_LocationBase):
"""
return Location._clean(value, INVALID_CHARS)
-
@staticmethod
def clean_keeping_underscores(value):
"""
@@ -82,7 +80,6 @@ class Location(_LocationBase):
"""
return INVALID_CHARS.sub('_', value)
-
@staticmethod
def clean_for_url_name(value):
"""
@@ -154,9 +151,7 @@ class Location(_LocationBase):
to mean wildcard selection.
"""
-
- if (org is None and course is None and category is None and
- name is None and revision is None):
+ if (org is None and course is None and category is None and name is None and revision is None):
location = loc_or_tag
else:
location = (loc_or_tag, org, course, category, name, revision)
@@ -191,7 +186,7 @@ class Location(_LocationBase):
match = MISSING_SLASH_URL_RE.match(location)
if match is None:
log.debug('location is instance of %s but no URL match' % basestring)
- raise InvalidLocationError(location)
+ raise InvalidLocationError(location)
groups = match.groupdict()
check_dict(groups)
return _LocationBase.__new__(_cls, **groups)
@@ -233,7 +228,7 @@ class Location(_LocationBase):
html id attributes
"""
s = "-".join(str(v) for v in self.list()
- if v is not None)
+ if v is not None)
return Location.clean_for_html(s)
def dict(self):
@@ -258,6 +253,12 @@ class Location(_LocationBase):
at the location URL hierachy"""
return "/".join([self.org, self.course, self.name])
+ def replace(self, **kwargs):
+ '''
+ Expose a public method for replacing location elements
+ '''
+ return self._replace(**kwargs)
+
class ModuleStore(object):
"""
@@ -382,12 +383,6 @@ class ModuleStore(object):
'''
raise NotImplementedError
- def get_course(self, course_id):
- '''
- Look for a specific course id. Returns the course descriptor, or None if not found.
- '''
- raise NotImplementedError
-
def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location in this
course. Needed for path_to_location().
@@ -406,8 +401,7 @@ class ModuleStore(object):
courses = [
course
for course in self.get_courses()
- if course.location.org == location.org
- and course.location.course == location.course
+ if course.location.org == location.org and course.location.course == location.course
]
return courses
diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py
index c3f1b23688..9262c5e9d6 100644
--- a/common/lib/xmodule/xmodule/modulestore/draft.py
+++ b/common/lib/xmodule/xmodule/modulestore/draft.py
@@ -13,11 +13,12 @@ def as_draft(location):
"""
return Location(location)._replace(revision=DRAFT)
+
def as_published(location):
"""
Returns the Location that is the published version for `location`
"""
- return Location(location)._replace(revision=None)
+ return Location(location)._replace(revision=None)
def wrap_draft(item):
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
index 31237af7b9..8cf148f742 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
@@ -3,7 +3,6 @@ from time import gmtime
from uuid import uuid4
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
-from xmodule.timeparse import stringify_time
from xmodule.modulestore.inheritance import own_metadata
diff --git a/common/lib/xmodule/xmodule/randomize_module.py b/common/lib/xmodule/xmodule/randomize_module.py
index 240f33e33e..434706530b 100644
--- a/common/lib/xmodule/xmodule/randomize_module.py
+++ b/common/lib/xmodule/xmodule/randomize_module.py
@@ -4,6 +4,8 @@ import random
from xmodule.x_module import XModule
from xmodule.seq_module import SequenceDescriptor
+from lxml import etree
+
from xblock.core import Scope, Integer
log = logging.getLogger('mitx.' + __name__)
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 7480cda0c5..2f54bbf405 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -136,6 +136,7 @@ class XmlDescriptor(XModuleDescriptor):
'hide_progress_tab': bool_map,
'allow_anonymous': bool_map,
'allow_anonymous_to_peers': bool_map,
+ 'show_timezone': bool_map,
}
diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html
index 9a1b3bed92..0133edadfa 100644
--- a/common/templates/jasmine/base.html
+++ b/common/templates/jasmine/base.html
@@ -12,6 +12,7 @@
+
{% load compressed %}
{# static files #}
@@ -37,15 +38,14 @@
+
@@ -44,30 +45,10 @@
diff --git a/common/test/data/full/course.xml b/common/test/data/full/course.xml
index 7a05db42f2..b2f9097020 100644
--- a/common/test/data/full/course.xml
+++ b/common/test/data/full/course.xml
@@ -1 +1 @@
-
+
diff --git a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml
index 26f8f5a08d..47b19f75ed 100644
--- a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml
+++ b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml
@@ -12,4 +12,13 @@
Minor correction: Six elements (five resistors)…
+
+
+
+
+
+
Inline content…
+
+
+
diff --git a/common/test/phantom-jasmine b/common/test/phantom-jasmine
deleted file mode 160000
index a54d435b55..0000000000
--- a/common/test/phantom-jasmine
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a54d435b5556650efbcdb0490e6c7928ac75238a
diff --git a/doc/testing.md b/doc/testing.md
index d6c7b7ee86..e5d035d90e 100644
--- a/doc/testing.md
+++ b/doc/testing.md
@@ -8,7 +8,7 @@ and acceptance tests.
### Unit Tests
* Each test case should be concise: setup, execute, check, and teardown.
-If you find yourself writing tests with many steps, consider refactoring
+If you find yourself writing tests with many steps, consider refactoring
the unit under tests into smaller units, and then testing those individually.
* As a rule of thumb, your unit tests should cover every code branch.
@@ -16,19 +16,19 @@ the unit under tests into smaller units, and then testing those individually.
* Mock or patch external dependencies.
We use [voidspace mock](http://www.voidspace.org.uk/python/mock/).
-* We unit test Python code (using [unittest](http://docs.python.org/2/library/unittest.html)) and
+* We unit test Python code (using [unittest](http://docs.python.org/2/library/unittest.html)) and
Javascript (using [Jasmine](http://pivotal.github.io/jasmine/))
### Integration Tests
* Test several units at the same time.
Note that you can still mock or patch dependencies
-that are not under test! For example, you might test that
-`LoncapaProblem`, `NumericalResponse`, and `CorrectMap` in the
+that are not under test! For example, you might test that
+`LoncapaProblem`, `NumericalResponse`, and `CorrectMap` in the
`capa` package work together, while still mocking out template rendering.
* Use integration tests to ensure that units are hooked up correctly.
-You do not need to test every possible input--that's what unit
-tests are for. Instead, focus on testing the "happy path"
+You do not need to test every possible input--that's what unit
+tests are for. Instead, focus on testing the "happy path"
to verify that the components work together correctly.
* Many of our tests use the [Django test client](https://docs.djangoproject.com/en/dev/topics/testing/overview/) to simulate
@@ -43,8 +43,8 @@ these tests simulate user interactions through the browser using
Overall, you want to write the tests that **maximize coverage**
while **minimizing maintenance**.
-In practice, this usually means investing heavily
-in unit tests, which tend to be the most robust to changes in the code base.
+In practice, this usually means investing heavily
+in unit tests, which tend to be the most robust to changes in the code base.

@@ -53,13 +53,13 @@ and acceptance tests. Most of our tests are unit tests or integration tests.
## Test Locations
-* Python unit and integration tests: Located in
+* Python unit and integration tests: Located in
subpackages called `tests`.
-For example, the tests for the `capa` package are located in
+For example, the tests for the `capa` package are located in
`common/lib/capa/capa/tests`.
* Javascript unit tests: Located in `spec` folders. For example,
-`common/lib/xmodule/xmodule/js/spec` and `{cms,lms}/static/coffee/spec`
+`common/lib/xmodule/xmodule/js/spec` and `{cms,lms}/static/coffee/spec`
For consistency, you should use the same directory structure for implementation
and test. For example, the test for `src/views/module.coffee`
should be written in `spec/views/module_spec.coffee`.
@@ -88,7 +88,7 @@ because the `capa` package handles problem XML.
Before running tests, ensure that you have all the dependencies. You can install dependencies using:
- pip install -r requirements.txt
+ rake install_prereqs
## Running Python Unit tests
@@ -101,7 +101,7 @@ You can run tests using `rake` commands. For example,
rake test
-runs all the tests. It also runs `collectstatic`, which prepares the static files used by the site (for example, compiling Coffeescript to Javascript).
+runs all the tests. It also runs `collectstatic`, which prepares the static files used by the site (for example, compiling Coffeescript to Javascript).
You can also run the tests without `collectstatic`, which tends to be faster:
@@ -117,12 +117,11 @@ xmodule can be tested independently, with this:
To run a single django test class:
- django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth
+ rake test_lms[courseware.tests.tests:testViewAuth]
To run a single django test:
- django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth.test_dark_launch
-
+ rake test_lms[courseware.tests.tests:TestViewAuth.test_dark_launch]
To run a single nose test file:
@@ -150,7 +149,7 @@ If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environme
PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms}
-Once you have run the `rake` command, your browser should open to
+Once you have run the `rake` command, your browser should open to
to `http://localhost/_jasmine/`, which displays the test results.
**Troubleshooting**: If you get an error message while running the `rake` task,
@@ -163,7 +162,7 @@ Most of our tests use [Splinter](http://splinter.cobrateam.info/)
to simulate UI browser interactions. Splinter, in turn,
uses [Selenium](http://docs.seleniumhq.org/) to control the Chrome browser.
-**Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver)
+**Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver)
installed to run the tests in Chrome. The tests are confirmed to run
with Chrome (not Chromium) version 26.0.0.1410.63 with ChromeDriver
version r195636.
@@ -184,13 +183,7 @@ To start the debugger on failure, add the `--pdb` option:
To run tests faster by not collecting static files, you can use
`rake fasttest_acceptance_lms` and `rake fasttest_acceptance_cms`.
-
-**Troubleshooting**: If you get an error message that says something about harvest not being a command, you probably are missing a requirement.
-Try running:
-
- pip install -r requirements.txt
-
-**Note**: The acceptance tests can *not* currently run in parallel.
+**Note**: The acceptance tests can *not* currently run in parallel.
## Viewing Test Coverage
diff --git a/jenkins/test.sh b/jenkins/test.sh
index d8cd2c1843..35be3a0121 100755
--- a/jenkins/test.sh
+++ b/jenkins/test.sh
@@ -73,8 +73,8 @@ rake pylint > pylint.log || cat pylint.log
TESTS_FAILED=0
# Run the python unit tests
-rake test_cms[false] || TESTS_FAILED=1
-rake test_lms[false] || TESTS_FAILED=1
+rake test_cms || TESTS_FAILED=1
+rake test_lms || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
@@ -82,7 +82,7 @@ rake test_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_lms || TESTS_FAILED=1
rake phantomjs_jasmine_cms || TESTS_FAILED=1
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
-rake phantomjs_jasmine_discussion || TESTS_FAILED=1
+rake phantomjs_jasmine_common/static/coffee || TESTS_FAILED=1
rake coverage:xml coverage:html
diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py
index e906fb5f7e..34e369c1ef 100644
--- a/lms/djangoapps/django_comment_client/base/views.py
+++ b/lms/djangoapps/django_comment_client/base/views.py
@@ -26,7 +26,7 @@ from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
-from django_comment_client.models import Role
+from django_comment_common.models import Role
from courseware.access import has_access
log = logging.getLogger(__name__)
diff --git a/lms/djangoapps/django_comment_client/management/commands/assign_role.py b/lms/djangoapps/django_comment_client/management/commands/assign_role.py
index 1be3bff719..4e9321410c 100644
--- a/lms/djangoapps/django_comment_client/management/commands/assign_role.py
+++ b/lms/djangoapps/django_comment_client/management/commands/assign_role.py
@@ -1,7 +1,7 @@
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
-from django_comment_client.models import Role
+from django_comment_common.models import Role
from django.contrib.auth.models import User
diff --git a/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py b/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py
index 72100738d9..9ef4f3d0b1 100644
--- a/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py
+++ b/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py
@@ -7,7 +7,7 @@ Enrollments.
from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment
-from django_comment_client.models import assign_default_role
+from django_comment_common.models import assign_default_role
class Command(BaseCommand):
diff --git a/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py b/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py
index d5ba0042fc..037bb292ec 100644
--- a/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py
+++ b/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py
@@ -7,7 +7,7 @@ Enrollments.
from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment
-from django_comment_client.models import assign_default_role
+from django_comment_common.models import assign_default_role
class Command(BaseCommand):
diff --git a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
index 9d6eefd11d..1073d7dbcf 100644
--- a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
+++ b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
@@ -1,5 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
-from django_comment_client.models import Role
+from django_comment_common.utils import seed_permissions_roles
class Command(BaseCommand):
@@ -13,26 +13,4 @@ class Command(BaseCommand):
raise CommandError("Too many arguments")
course_id = args[0]
- administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
- moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
- community_ta_role = Role.objects.get_or_create(name="Community TA", course_id=course_id)[0]
- student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
-
- for per in ["vote", "update_thread", "follow_thread", "unfollow_thread",
- "update_comment", "create_sub_comment", "unvote", "create_thread",
- "follow_commentable", "unfollow_commentable", "create_comment", ]:
- student_role.add_permission(per)
-
- for per in ["edit_content", "delete_thread", "openclose_thread",
- "endorse_comment", "delete_comment", "see_all_cohorts"]:
- moderator_role.add_permission(per)
-
- for per in ["manage_moderator"]:
- administrator_role.add_permission(per)
-
- moderator_role.inherit_permissions(student_role)
-
- # For now, Community TA == Moderator, except for the styling.
- community_ta_role.inherit_permissions(moderator_role)
-
- administrator_role.inherit_permissions(moderator_role)
+ seed_permissions_roles(course_id)
diff --git a/lms/djangoapps/django_comment_client/management/commands/show_permissions.py b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py
index f24f183193..67fc29ea97 100644
--- a/lms/djangoapps/django_comment_client/management/commands/show_permissions.py
+++ b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py
@@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
+from django_comment_common.models import Permission, Role
from django.contrib.auth.models import User
diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py
index 71e7a81f68..76d27be3bf 100644
--- a/lms/djangoapps/django_comment_client/models.py
+++ b/lms/djangoapps/django_comment_client/models.py
@@ -1,64 +1 @@
-import logging
-
-from django.db import models
-from django.contrib.auth.models import User
-
-from django.dispatch import receiver
-from django.db.models.signals import post_save
-
-from student.models import CourseEnrollment
-
-from courseware.courses import get_course_by_id
-
-FORUM_ROLE_ADMINISTRATOR = 'Administrator'
-FORUM_ROLE_MODERATOR = 'Moderator'
-FORUM_ROLE_COMMUNITY_TA = 'Community TA'
-FORUM_ROLE_STUDENT = 'Student'
-
-
-@receiver(post_save, sender=CourseEnrollment)
-def assign_default_role(sender, instance, **kwargs):
- if instance.user.is_staff:
- role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
- else:
- role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
-
- logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
- instance.user.roles.add(role)
-
-
-class Role(models.Model):
- name = models.CharField(max_length=30, null=False, blank=False)
- users = models.ManyToManyField(User, related_name="roles")
- course_id = models.CharField(max_length=255, blank=True, db_index=True)
-
- def __unicode__(self):
- return self.name + " for " + (self.course_id if self.course_id else "all courses")
-
- def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
- # since it's one-off and doesn't handle inheritance later
- if role.course_id and role.course_id != self.course_id:
- logging.warning("%s cannot inherit permissions from %s due to course_id inconsistency",
- self, role)
- for per in role.permissions.all():
- self.add_permission(per)
-
- def add_permission(self, permission):
- self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
-
- def has_permission(self, permission):
- course = get_course_by_id(self.course_id)
- if self.name == FORUM_ROLE_STUDENT and \
- (permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
- (not course.forum_posts_allowed):
- return False
-
- return self.permissions.filter(name=permission).exists()
-
-
-class Permission(models.Model):
- name = models.CharField(max_length=30, null=False, blank=False, primary_key=True)
- roles = models.ManyToManyField(Role, related_name="permissions")
-
- def __unicode__(self):
- return self.name
+# This file is intentionally blank. It has been moved to common/djangoapps/django_comment_common
diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py
index cc3ead53e7..1a523a170a 100644
--- a/lms/djangoapps/django_comment_client/permissions.py
+++ b/lms/djangoapps/django_comment_client/permissions.py
@@ -1,4 +1,4 @@
-from .models import Role, Permission
+from django_comment_common.models import Role, Permission
from django.db.models.signals import post_save
from django.dispatch import receiver
from student.models import CourseEnrollment
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index a5cfce4dc7..8fd8ed7e2b 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -6,7 +6,7 @@ from django.test import TestCase
from student.models import CourseEnrollment
from django_comment_client.permissions import has_permission
-from django_comment_client.models import Role
+from django_comment_common.models import Role
class PermissionsTestCase(TestCase):
diff --git a/lms/djangoapps/django_comment_client/tests/factories.py b/lms/djangoapps/django_comment_client/tests/factories.py
index eb1d9477c3..4a82c8f1bb 100644
--- a/lms/djangoapps/django_comment_client/tests/factories.py
+++ b/lms/djangoapps/django_comment_client/tests/factories.py
@@ -1,5 +1,5 @@
from factory import DjangoModelFactory
-from django_comment_client.models import Role, Permission
+from django_comment_common.models import Role, Permission
class RoleFactory(DjangoModelFactory):
diff --git a/lms/djangoapps/django_comment_client/tests/test_models.py b/lms/djangoapps/django_comment_client/tests/test_models.py
index 0835c841e2..e45c883931 100644
--- a/lms/djangoapps/django_comment_client/tests/test_models.py
+++ b/lms/djangoapps/django_comment_client/tests/test_models.py
@@ -1,4 +1,4 @@
-import django_comment_client.models as models
+import django_comment_common.models as models
import django_comment_client.permissions as permissions
from django.test import TestCase
diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py
index a7c0ce0a39..555264cb5f 100644
--- a/lms/djangoapps/django_comment_client/tests/test_utils.py
+++ b/lms/djangoapps/django_comment_client/tests/test_utils.py
@@ -1,6 +1,6 @@
from django.test import TestCase
from student.tests.factories import UserFactory, CourseEnrollmentFactory
-
+from django_comment_common.models import Role, Permission
from factories import RoleFactory
import django_comment_client.utils as utils
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 0363607cfe..276956f0e9 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -14,7 +14,7 @@ from django.core.urlresolvers import reverse
from django.db import connection
from django.http import HttpResponse
from django.utils import simplejson
-from django_comment_client.models import Role
+from django_comment_common.models import Role
from django_comment_client.permissions import check_permissions_by_view
from xmodule.modulestore.exceptions import NoPathToItem
diff --git a/lms/djangoapps/instructor/tests/test_forum_admin.py b/lms/djangoapps/instructor/tests/test_forum_admin.py
index d2d58fb61c..7b4e729867 100644
--- a/lms/djangoapps/instructor/tests/test_forum_admin.py
+++ b/lms/djangoapps/instructor/tests/test_forum_admin.py
@@ -9,7 +9,7 @@ from django.test.utils import override_settings
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
-from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
+from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
from django_comment_client.utils import has_forum_access
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index dd6748e691..00b1b918b3 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -27,7 +27,7 @@ from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name)
from courseware.courses import get_course_with_access
from courseware.models import StudentModule
-from django_comment_client.models import (Role,
+from django_comment_common.models import (Role,
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
FORUM_ROLE_COMMUNITY_TA)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 741d624ed7..e7bc9519d9 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -700,8 +700,7 @@ INSTALLED_APPS = (
# Discussion forums
'django_comment_client',
-
- # Student notes
+ 'django_comment_common',
'notes',
)
diff --git a/lms/envs/jasmine.py b/lms/envs/jasmine.py
index 2c30bc7de7..4a78ed8075 100644
--- a/lms/envs/jasmine.py
+++ b/lms/envs/jasmine.py
@@ -36,7 +36,12 @@ PIPELINE_JS['spec'] = {
}
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
+JASMINE_REPORT_DIR = os.environ.get('JASMINE_REPORT_DIR', 'reports/lms/jasmine')
+
+TEMPLATE_CONTEXT_PROCESSORS += ('settings_context_processor.context_processors.settings',)
+TEMPLATE_VISIBLE_SETTINGS = ('JASMINE_REPORT_DIR', )
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
+STATICFILES_DIRS.append(REPO_ROOT/'node_modules/jasmine-reporters/src')
-INSTALLED_APPS += ('django_jasmine', )
+INSTALLED_APPS += ('django_jasmine', 'settings_context_processor')
diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss
index e62dd12541..6f43a02df7 100644
--- a/lms/static/sass/base/_base.scss
+++ b/lms/static/sass/base/_base.scss
@@ -2,8 +2,8 @@
// overflow-y: scroll;
// }
-body {
- background: rgb(250,250,250);
+html, body {
+ background: $body-bg;
font-family: $sans-serif;
font-size: 1em;
font-style: normal;
@@ -61,20 +61,20 @@ p + p, ul + p, ol + p {
p {
a:link, a:visited {
- color: $blue;
+ color: $link-color;
font: normal 1em/1em $serif;
text-decoration: none;
@include transition(all, 0.1s, linear);
&:hover {
- color: $blue;
+ color: $link-color;
text-decoration: underline;
}
}
}
a:link, a:visited {
- color: $blue;
+ color: $link-color;
font: normal 1em/1em $sans-serif;
text-decoration: none;
@include transition(all, 0.1s, linear);
@@ -87,8 +87,8 @@ a:link, a:visited {
.content-wrapper {
width: flex-grid(12);
margin: 0 auto;
+ background: $content-wrapper-bg;
padding-bottom: ($baseline*2);
- background: rgb(255,255,255);
}
.container {
@@ -164,7 +164,7 @@ mark {
display: none;
padding: 10px;
@include linear-gradient(top, rgba(0, 0, 0, .1), rgba(0, 0, 0, .0));
- background-color: $pink;
+ background-color: $site-status-color;
box-shadow: 0 -1px 0 rgba(0, 0, 0, .3) inset;
font-size: 14px;
diff --git a/lms/static/sass/base/_extends.scss b/lms/static/sass/base/_extends.scss
index 2998e25dca..d244eff55f 100644
--- a/lms/static/sass/base/_extends.scss
+++ b/lms/static/sass/base/_extends.scss
@@ -1,39 +1,30 @@
.faded-hr-divider {
- @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
- rgba(200,200,200, 1) 50%,
- rgba(200,200,200, 0)));
+ @include background-image($faded-hr-image-1);
height: 1px;
width: 100%;
}
.faded-hr-divider-medium {
- @include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
- rgba(240,240,240, 1) 50%,
- rgba(240,240,240, 0)));
+ @include background-image($faded-hr-image-4);
height: 1px;
width: 100%;
}
.faded-hr-divider-light {
- @include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
- rgba(255,255,255, 0.8) 50%,
- rgba(255,255,255, 0)));
+ @include background-image($faded-hr-image-5);
height: 1px;
width: 100%;
}
.faded-vertical-divider {
- @include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
- rgba(200,200,200, 1) 50%,
- rgba(200,200,200, 0)));
+ @include background-image($faded-hr-image-1);
height: 100%;
width: 1px;
}
.faded-vertical-divider-light {
- @include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
- rgba(255,255,255, 0.6) 50%,
- rgba(255,255,255, 0)));
+ @include background-image($faded-hr-image-6);
+ background: transparent;
height: 100%;
width: 1px;
}
@@ -66,14 +57,12 @@
}
.fade-right-hr-divider {
- @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
- rgba(200,200,200, 1)));
+ @include background-image($faded-hr-image-2);
border: none;
}
.fade-left-hr-divider {
- @include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
- rgba(200,200,200, 0)));
+ @include background-image($faded-hr-image-3);
border: none;
}
diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss
index ddbd930323..6bd593c28c 100644
--- a/lms/static/sass/base/_variables.scss
+++ b/lms/static/sass/base/_variables.scss
@@ -14,6 +14,14 @@ $monospace: Monaco, 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
$body-font-family: $sans-serif;
$serif: $georgia;
+$body-font-size: em(14);
+$body-line-height: golden-ratio(.875em, 1);
+$base-font-color: rgb(60,60,60);
+$baseFontColor: rgb(60,60,60);
+$base-font-color: rgb(60,60,60);
+$lighter-base-font-color: rgb(100,100,100);
+$very-light-text: #fff;
+
$white: rgb(255,255,255);
$black: rgb(0,0,0);
$blue: rgb(29,157,217);
@@ -52,6 +60,66 @@ $baseFontColor: rgb(60,60,60);
$lighter-base-font-color: rgb(100,100,100);
$text-color: $dark-gray;
-$body-font-family: $sans-serif;
-$body-font-size: em(14);
-$body-line-height: golden-ratio(.875em, 1);
+$body-bg: rgb(250,250,250);
+$header-image: linear-gradient(-90deg, rgba(255,255,255, 1), rgba(230,230,230, 0.9));
+$header-bg: transparent;
+$courseware-header-image: linear-gradient(top, #fff, #eee);
+$courseware-header-bg: transparent;
+$footer-bg: transparent;
+$courseware-footer-border: none;
+$courseware-footer-shadow: none;
+$courseware-footer-margin: 0px;
+
+$button-bg-image: linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%);
+$button-bg-color: transparent;
+$button-bg-hover-color: #fff;
+
+$faded-hr-image-1: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1) 50%, rgba(200,200,200, 0));
+$faded-hr-image-2: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1));
+$faded-hr-image-3: linear-gradient(180deg, rgba(200,200,200, 1) 0%, rgba(200,200,200, 0));
+$faded-hr-image-4: linear-gradient(180deg, rgba(240,240,240, 0) 0%, rgba(240,240,240, 1) 50%, rgba(240,240,240, 0));
+$faded-hr-image-5: linear-gradient(180deg, rgba(255,255,255, 0) 0%, rgba(255,255,255, 0.8) 50%, rgba(255,255,255, 0));
+$faded-hr-image-6: linear-gradient(90deg, rgba(255,255,255, 0) 0%, rgba(255,255,255, 0.6) 50%, rgba(255,255,255, 0));
+
+$dashboard-profile-header-image: linear-gradient(-90deg, rgb(255,255,255), rgb(245,245,245));
+$dashboard-profile-header-color: transparent;
+$dashboard-profile-color: rgb(252,252,252);
+$dot-color: $light-gray;
+
+$content-wrapper-bg: rgb(255,255,255);
+$course-bg-color: #d6d6d6;
+$course-bg-image: url(../images/bg-texture.png);
+
+$course-profile-bg: rgb(245,245,245);
+$course-header-bg: rgba(255,255,255, 0.93);
+
+$border-color-1: rgb(190,190,190);
+$border-color-2: rgb(200,200,200);
+$border-color-3: rgb(100,100,100);
+$border-color-4: rgb(252,252,252);
+
+$link-color: $blue;
+$link-hover: $pink;
+$selection-color-1: $pink;
+$selection-color-2: #444;
+$site-status-color: $pink;
+
+$button-color: $blue;
+$button-archive-color: #eee;
+
+$shadow-color: $blue;
+
+$sidebar-chapter-bg-top: rgba(255, 255, 255, .6);
+$sidebar-chapter-bg-bottom: rgba(255, 255, 255, 0);
+$sidebar-chapter-bg: #eee;
+$sidebar-active-image: linear-gradient(top, #e6e6e6, #d6d6d6);
+
+$form-bg-color: #fff;
+$modal-bg-color: rgb(245,245,245);
+
+//-----------------
+// CSS BG Images
+//-----------------
+$homepage-bg-image: '../images/homepage-bg.jpg';
+
+$video-thumb-url: '../images/courses/video-thumb.jpg';
\ No newline at end of file
diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss
index bfd90505cf..741a7f9a22 100644
--- a/lms/static/sass/course/_info.scss
+++ b/lms/static/sass/course/_info.scss
@@ -117,7 +117,7 @@ div.info-wrapper {
@include transition(all .2s);
h4 {
- color: $blue;
+ color: $link-color;
font-size: 1em;
font-weight: normal;
padding-left: 30px;
diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss
index 6183c8a675..584412ca22 100644
--- a/lms/static/sass/course/base/_base.scss
+++ b/lms/static/sass/course/base/_base.scss
@@ -1,7 +1,8 @@
body {
min-width: 980px;
min-height: 100%;
- background: url(../images/bg-texture.png) #d6d6d6;
+ background-image: $course-bg-image;
+ background-color: $course-bg-color;
}
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a, label {
@@ -34,7 +35,7 @@ a {
width: 100%;
border-radius: 3px;
border: 1px solid $outer-border-color;
- background: #fff;
+ background: $body-bg;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
}
}
@@ -49,8 +50,8 @@ textarea,
input[type="text"],
input[type="email"],
input[type="password"] {
- background: rgb(250,250,250);
- border: 1px solid rgb(200,200,200);
+ background: $body-bg;
+ border: 1px solid $border-color-2;
@include border-radius(0);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
@include box-sizing(border-box);
@@ -65,7 +66,7 @@ input[type="password"] {
}
&:focus {
- border-color: lighten($blue, 20%);
+ border-color: lighten($link-color, 20%);
@include box-shadow(0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15));
outline: none;
}
@@ -94,7 +95,7 @@ img {
}
::selection, ::-moz-selection, ::-webkit-selection {
- background: #444;
+ background: $selection-color-2;
color: #fff;
}
@@ -143,7 +144,7 @@ img {
max-width: 350px;
padding: 15px 20px 17px;
border-radius: 3px;
- border: 1px solid #333;
+ border: 1px solid $border-color-3;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .1), rgba(255, 255, 255, 0)) rgba(30, 30, 30, .92);
box-shadow: 0 1px 3px rgba(0, 0, 0, .3), 0 1px 0 rgba(255, 255, 255, .1) inset;
font-size: 13px;
diff --git a/lms/static/sass/course/base/_extends.scss b/lms/static/sass/course/base/_extends.scss
index bcb93a3645..a94a9511fe 100644
--- a/lms/static/sass/course/base/_extends.scss
+++ b/lms/static/sass/course/base/_extends.scss
@@ -1,5 +1,5 @@
h1.top-header {
- border-bottom: 1px solid #e3e3e3;
+ border-bottom: 1px solid $border-color-2;
text-align: left;
font-size: em(24);
font-weight: 100;
diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss
index 81b497d4f9..6cf6f6a602 100644
--- a/lms/static/sass/course/courseware/_sidebar.scss
+++ b/lms/static/sass/course/courseware/_sidebar.scss
@@ -2,7 +2,7 @@ section.course-index {
@extend .sidebar;
@extend .tran;
@include border-radius(3px 0 0 3px);
- border-right: 1px solid #ddd;
+ border-right: 1px solid $border-color-2;
#open_close_accordion {
display: none;
@@ -70,8 +70,8 @@ section.course-index {
width: 100% !important;
@include box-sizing(border-box);
padding: 11px 14px;
- @include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0));
- background-color: #eee;
+ @include linear-gradient(top, $sidebar-chapter-bg-top, $sidebar-chapter-bg-bottom);
+ background-color: $sidebar-chapter-bg;
@include box-shadow(0 1px 0 #fff inset, 0 -1px 0 rgba(0, 0, 0, .1) inset);
@include transition(background-color .1s);
@@ -169,9 +169,9 @@ section.course-index {
}
> a {
- border: 1px solid #bbb;
+ border: 1px solid $border-color-1;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .35) inset);
- @include linear-gradient(top, #e6e6e6, #d6d6d6);
+ background: $sidebar-active-image;
&:after {
opacity: 1;
diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss
index e27a6e99d8..4d8f000668 100644
--- a/lms/static/sass/course/layout/_courseware_header.scss
+++ b/lms/static/sass/course/layout/_courseware_header.scss
@@ -75,9 +75,9 @@ header.global.slim {
login {
display: block;
- @include background-image(linear-gradient(-90deg, lighten($blue, 8%), lighten($blue, 5%) 50%, $blue 50%, darken($blue, 10%) 100%));
+ @include background-image(linear-gradient(-90deg, lighten($link-color, 8%), lighten($link-color, 5%) 50%, $link-color 50%, darken($link-color, 10%) 100%));
border: 1px solid transparent;
- border-color: darken($blue, 10%);
+ border-color: darken($link-color, 10%);
@include border-radius(3px);
@include box-sizing(border-box);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
@@ -97,7 +97,7 @@ header.global.slim {
vertical-align: middle;
&:hover, &.active {
- @include background-image(linear-gradient(-90deg, $blue, $blue 50%, $blue 50%, $blue 100%));
+ @include background-image(linear-gradient(-90deg, $link-color, $link-color 50%, $link-color 50%, $link-color 100%));
}
}
}
diff --git a/lms/static/sass/course/layout/_footer.scss b/lms/static/sass/course/layout/_footer.scss
index 7abf35a819..699846e781 100644
--- a/lms/static/sass/course/layout/_footer.scss
+++ b/lms/static/sass/course/layout/_footer.scss
@@ -1,4 +1,5 @@
footer {
- border: none;
- box-shadow: none;
+ border: $courseware-footer-border;
+ box-shadow: $courseware-footer-shadow;
+ margin-top: $courseware-footer-margin;
}
\ No newline at end of file
diff --git a/lms/static/sass/course/wiki/_wiki.scss b/lms/static/sass/course/wiki/_wiki.scss
index 1bc38abd9a..d064b6d345 100644
--- a/lms/static/sass/course/wiki/_wiki.scss
+++ b/lms/static/sass/course/wiki/_wiki.scss
@@ -113,7 +113,7 @@ section.wiki {
}
&:focus {
- border-color: $blue;
+ border-color: $link-color;
}
}
}
@@ -276,7 +276,7 @@ section.wiki {
li {
&.active {
a {
- color: $blue;
+ color: $link-color;
.icon-view,
.icon-home {
diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss
index 195760721e..9eab7c0a4f 100644
--- a/lms/static/sass/multicourse/_course_about.scss
+++ b/lms/static/sass/multicourse/_course_about.scss
@@ -4,11 +4,11 @@
}
header.course-profile {
- background: rgb(245,245,245);
- @include background-image(url('/static/images/homepage-bg.jpg'));
+ background: $course-profile-bg;
+ @include background-image(url($homepage-bg-image));
background-size: cover;
@include box-shadow(0 1px 80px 0 rgba(0,0,0, 0.5));
- border-bottom: 1px solid rgb(100,100,100);
+ border-bottom: 1px solid $border-color-3;
@include box-shadow(inset 0 1px 5px 0 rgba(0,0,0, 0.1));
height: 280px;
margin-top: -69px;
@@ -18,8 +18,8 @@
width: 100%;
.intro-inner-wrapper {
- background: rgba(255,255,255, 0.93);
- border: 1px solid rgb(100,100,100);
+ background: $course-header-bg;
+ border: 1px solid $border-color-3;
@include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
@include box-sizing(border-box);
@include clearfix;
@@ -44,7 +44,7 @@
z-index: 2;
> hgroup {
- border-bottom: 1px solid rgb(210,210,210);
+ border-bottom: 1px solid $border-color-2;
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
margin-bottom: 20px;
padding-bottom: 20px;
@@ -68,7 +68,7 @@
text-transform: none;
&:hover {
- color: $blue;
+ color: $link-color;
}
}
}
@@ -85,7 +85,7 @@
text-transform: none;
&:hover {
- color: $blue;
+ color: $link-color;
}
}
}
@@ -99,7 +99,7 @@
width: flex-grid(12);
> a.find-courses, a.register {
- @include button(shiny, $blue);
+ @include button(shiny, $button-color);
@include box-sizing(border-box);
@include border-radius(3px);
display: block;
@@ -122,7 +122,7 @@
}
strong {
- @include button(shiny, $blue);
+ @include button(shiny, $button-color);
@include box-sizing(border-box);
@include border-radius(3px);
display: block;
@@ -140,10 +140,10 @@
}
span.register {
- background: lighten($blue, 20%);
- border: 1px solid $blue;
+ background: $button-archive-color;
+ border: 1px solid darken($button-archive-color, 50%);
@include box-sizing(border-box);
- color: darken($blue, 20%);
+ color: darken($button-archive-color, 50%);
display: block;
letter-spacing: 1px;
padding: 10px 0px 8px;
@@ -176,7 +176,7 @@
z-index: 2;
.hero {
- border: 1px solid rgb(100,100,100);
+ border: 1px solid $border-color-3;
height: 100%;
overflow: hidden;
position: relative;
@@ -235,7 +235,7 @@
@include clearfix;
nav {
- border-bottom: 1px solid rgb(220,220,220);
+ border-bottom: 1px solid $border-color-2;
@include box-sizing(border-box);
@include clearfix;
margin: 40px 0;
@@ -262,7 +262,7 @@
}
&:hover, &.active {
- border-color: rgb(200,200,200);
+ border-color: $border-color-2;
color: $base-font-color;
text-decoration: none;
}
@@ -296,7 +296,7 @@
.teacher-image {
background: rgb(255,255,255);
- border: 1px solid rgb(200,200,200);
+ border: 1px solid $border-color-2;
height: 115px;
float: left;
margin: 0 15px 0px 0;
@@ -351,7 +351,7 @@
> section {
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15));
- border: 1px solid rgb(200,200,200);
+ border: 1px solid $border-color-2;
&.course-summary {
padding: 16px 20px 30px;
@@ -401,7 +401,7 @@
}
a.university-name {
- border-right: 1px solid rgb(200,200,200);
+ border-right: 1px solid $border-color-2;
color: $base-font-color;
font-family: $sans-serif;
font-style: italic;
@@ -498,12 +498,12 @@
li {
@include clearfix;
- border-bottom: 1px dotted rgb(220,220,220);
+ border-bottom: 1px dotted $border-color-2;
margin-bottom: 20px;
padding-bottom: 10px;
&.prerequisites {
- border: 1px solid rgb(220,220,220);
+ border: 1px solid $border-color-2;
margin: 0 -10px 0;
padding: 10px;
diff --git a/lms/static/sass/multicourse/_courses.scss b/lms/static/sass/multicourse/_courses.scss
index 45ecfcd23f..ac31da4d2a 100644
--- a/lms/static/sass/multicourse/_courses.scss
+++ b/lms/static/sass/multicourse/_courses.scss
@@ -1,12 +1,13 @@
.find-courses, .university-profile {
- background: rgb(252,252,252);
+ background: $course-profile-bg;
padding-bottom: 60px;
header.search {
- background: rgb(240,240,240);
+ background: $course-profile-bg;
background-size: cover;
+ @include background-image(url($homepage-bg-image));
background-position: center top !important;
- border-bottom: 1px solid rgb(100,100,100);
+ border-bottom: 1px solid $border-color-3;
@include box-shadow(inset 0 -1px 8px 0 rgba(0,0,0, 0.2), inset 0 1px 12px 0 rgba(0,0,0, 0.3));
height: 430px;
margin-top: -69px;
@@ -24,8 +25,8 @@
> hgroup {
background: #FFF;
- background: rgba(255,255,255, 0.93);
- border: 1px solid rgb(100,100,100);
+ background: $course-header-bg;
+ border: 1px solid $border-color-3;
@include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
padding: 20px 30px;
position: relative;
@@ -83,7 +84,7 @@
}
section.message {
- border-top: 1px solid rgb(220,220,220);
+ border-top: 1px solid $border-color-2;
@include clearfix;
margin-top: 20px;
padding-top: 60px;
diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss
index cc54b9b242..b173647550 100644
--- a/lms/static/sass/multicourse/_dashboard.scss
+++ b/lms/static/sass/multicourse/_dashboard.scss
@@ -30,8 +30,9 @@
width: flex-grid(3);
header.profile {
- @include background-image(linear-gradient(-90deg, rgb(255,255,255), rgb(245,245,245)));
- border: 1px solid rgb(200,200,200);
+ @include background-image($dashboard-profile-header-image);
+ background-color: $dashboard-profile-header-color;
+ border: 1px solid $border-color-2;
@include border-radius(4px);
@include box-sizing(border-box);
width: flex-grid(12);
@@ -53,8 +54,8 @@
padding: 0px 10px;
> ul {
- background: rgb(252,252,252);
- border: 1px solid rgb(200,200,200);
+ background: $dashboard-profile-color;
+ border: 1px solid $border-color-2;
border-top: none;
//@include border-bottom-radius(4px);
@include box-sizing(border-box);
@@ -66,7 +67,7 @@
li {
@include clearfix;
- border-bottom: 1px dotted rgb(220,220,220);
+ border-bottom: 1px dotted $border-color-2;
list-style: none;
margin-bottom: 15px;
padding-bottom: 17px;
@@ -128,8 +129,8 @@
.news-carousel {
@include clearfix;
margin: 30px 10px 0;
- border: 1px solid rgb(200,200,200);
- background: rgb(252,252,252);
+ border: 1px solid $border-color-2;
+ background: $dashboard-profile-color;
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15));
* {
@@ -156,14 +157,14 @@
width: 11px;
height: 11px;
border-radius: 11px;
- background: $light-gray;
+ background: $dot-color;
&:hover {
- background: #ccc;
+ background: $lighter-base-font-color;
}
&.current {
- background: $blue;
+ background: $link-color;
}
}
@@ -201,7 +202,7 @@
img {
width: 100%;
- border: 1px solid $light-gray;
+ border: 1px solid $border-color-1;
}
}
@@ -229,7 +230,7 @@
width: flex-grid(9);
> header {
- border-bottom: 1px solid rgb(210,210,210);
+ border-bottom: 1px solid $border-color-2;
margin-bottom: 30px;
}
@@ -246,8 +247,9 @@
a {
background: rgb(240,240,240);
- @include background-image(linear-gradient(-90deg, rgb(245,245,245) 0%, rgb(243,243,243) 50%, rgb(237,237,237) 50%, rgb(235,235,235) 100%));
- border: 1px solid rgb(220,220,220);
+ @include background-image($button-bg-image);
+ background-color: $button-bg-color;
+ border: 1px solid $border-color-2;
@include border-radius(4px);
@include box-shadow(0 1px 8px 0 rgba(0,0,0, 0.1));
@include box-sizing(border-box);
@@ -260,7 +262,7 @@
text-shadow: 0 1px rgba(255,255,255, 0.6);
&:hover {
- color: $blue;
+ color: $link-color;
text-decoration: none;
}
}
@@ -272,7 +274,7 @@
margin-right: flex-gutter();
margin-bottom: 50px;
padding-bottom: 50px;
- border-bottom: 1px solid $light-gray;
+ border-bottom: 1px solid $border-color-1;
position: relative;
width: flex-grid(12);
z-index: 20;
@@ -343,7 +345,7 @@
.course-status {
background: $yellow;
- border: 1px solid rgb(200,200,200);
+ border: 1px solid $border-color-2;
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
margin-top: 17px;
margin-right: flex-gutter();
@@ -362,7 +364,7 @@
.course-status-completed {
background: #ccc;
- color: #fff;
+ color: $very-light-text;
p {
color: #222;
@@ -374,7 +376,7 @@
}
.enter-course {
- @include button(simple, $blue);
+ @include button(simple, $button-color);
@include box-sizing(border-box);
@include border-radius(3px);
display: block;
@@ -386,7 +388,7 @@
margin-top: 16px;
&.archived {
- @include button(simple, #eee);
+ @include button(simple, $button-archive-color);
font: normal 15px/1.6rem $sans-serif;
padding: 6px 32px 7px;
diff --git a/lms/static/sass/multicourse/_home.scss b/lms/static/sass/multicourse/_home.scss
index b5546aa470..ea8ddaf654 100644
--- a/lms/static/sass/multicourse/_home.scss
+++ b/lms/static/sass/multicourse/_home.scss
@@ -7,15 +7,15 @@
}
> header {
- background: rgb(255,255,255);
- @include background-image(url('/static/images/homepage-bg.jpg'));
+ background: $dashboard-profile-color;
+ @include background-image(url($homepage-bg-image));
background-size: cover;
- border-bottom: 1px solid rgb(80,80,80);
- @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.9), inset 0 -1px 5px 0 rgba(0,0,0, 0.1));
+ border-bottom: 1px solid $border-color-3;
+ @include box-shadow(0 1px 0 0 $course-header-bg, inset 0 -1px 5px 0 rgba(0,0,0, 0.1));
@include clearfix;
height: 460px;
- margin-top: -69px;
overflow: hidden;
+ margin-top: -69px;
padding: 0px;
width: flex-grid(12);
@@ -31,8 +31,8 @@
.title {
background: #FFF;
- background: rgba(255,255,255, 0.93);
- border: 1px solid rgb(100,100,100);
+ background: $course-header-bg;
+ border: 1px solid $border-color-3;
@include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
@include box-sizing(border-box);
min-height: 120px;
@@ -80,8 +80,8 @@
.media {
background: #FFF;
- background: rgba(255,255,255, 0.93);
- border: 1px solid rgb(100,100,100);
+ background: $course-header-bg;
+ border: 1px solid $border-color-3;
border-left: 0;
@include box-sizing(border-box);
// @include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
@@ -101,7 +101,7 @@
height: 100%;
overflow: hidden;
position: relative;
- background: url('../images/courses/video-thumb.jpg') center no-repeat;
+ background: url($video-thumb-url) center no-repeat;
@include background-size(cover);
.play-intro {
@@ -164,9 +164,9 @@
> h2 {
@include background-image(linear-gradient(-90deg, rgb(250,250,250), rgb(230,230,230)));
- border: 1px solid rgb(200,200,200);
+ border: 1px solid $border-color-2;
@include border-radius(4px);
- border-top-color: rgb(190,190,190);
+ border-top-color: $border-color-1;
@include box-shadow(inset 0 0 0 1px rgba(255,255,255, 0.4), 0 0px 12px 0 rgba(0,0,0, 0.2));
color: $lighter-base-font-color;
letter-spacing: 1px;
@@ -180,7 +180,7 @@
}
.university-partners {
- border-bottom: 1px solid rgb(210,210,210);
+ border-bottom: 1px solid $border-color-2;
margin-bottom: 0px;
overflow: hidden;
position: relative;
@@ -366,13 +366,13 @@
}
.more-info {
- border: 1px solid rgb(200,200,200);
+ border: 1px solid $border-color-2;
margin-bottom: 80px;
width: flex-grid(12);
header {
@include background-image(linear-gradient(-90deg, rgb(250,250,250), rgb(230,230,230)));
- border-bottom: 1px solid rgb(200,200,200);
+ border-bottom: 1px solid $border-color-2;
@include clearfix;
padding: 10px 20px 8px;
position: relative;
@@ -415,14 +415,14 @@
width: flex-grid(12);
.blog-posts {
- border-bottom: 1px solid rgb(220,220,220);
+ border-bottom: 1px solid $border-color-2;
margin-bottom: 20px;
padding-bottom: 20px;
@include clearfix;
> article {
border: 1px dotted transparent;
- border-color: rgb(220,220,220);
+ border-color: $border-color-2;
@include box-sizing(border-box);
@include clearfix;
float: left;
@@ -432,8 +432,8 @@
width: flex-grid(4);
&:hover {
- background: rgb(248,248,248);
- border: 1px solid rgb(220,220,220);
+ background: $body-bg;
+ border: 1px solid $border-color-2;
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.1));
}
@@ -442,7 +442,7 @@
}
.post-graphics {
- border: 1px solid rgb(190,190,190);
+ border: 1px solid $border-color-1;
@include box-sizing(border-box);
display: block;
float: left;
diff --git a/lms/static/sass/shared/_course_object.scss b/lms/static/sass/shared/_course_object.scss
index e99559a49f..f78c483925 100644
--- a/lms/static/sass/shared/_course_object.scss
+++ b/lms/static/sass/shared/_course_object.scss
@@ -31,8 +31,8 @@
}
.course {
- background: rgb(250,250,250);
- border: 1px solid rgb(180,180,180);
+ background: $body-bg;
+ border: 1px solid $border-color-1;
@include border-radius(2px);
@include box-sizing(border-box);
@include box-shadow(0 1px 10px 0 rgba(0,0,0, 0.15), inset 0 0 0 1px rgba(255,255,255, 0.9));
@@ -42,7 +42,7 @@
@include transition(all, 0.15s, linear);
.status {
- background: $blue;
+ background: $link-color;
color: white;
font-size: 10px;
left: 10px;
@@ -55,7 +55,7 @@
}
.status:after {
- border-bottom: 6px solid shade($blue, 50%);
+ border-bottom: 6px solid shade($link-color, 50%);
border-right: 6px solid transparent;
content: "";
display: block;
@@ -90,7 +90,7 @@
}
.inner-wrapper {
- border: 1px solid rgba(255,255,255, 1);
+ border: 1px solid $border-color-4;
height: 100%;
height: 200px;
overflow: hidden;
@@ -116,12 +116,12 @@
text-decoration: none;
.info-link {
- color: $blue;
+ color: $link-color;
opacity: 1;
}
h2 {
- color: $blue;
+ color: $link-color;
}
}
@@ -176,7 +176,7 @@
// }
.info {
- background: rgb(255,255,255);
+ background: $content-wrapper-bg;
height: 220px + 130px;
left: 0px;
position: absolute;
@@ -221,14 +221,14 @@
width: 100%;
.university {
- border-right: 1px solid rgb(200,200,200);
+ border-right: 1px solid $border-color-2;
color: $lighter-base-font-color;
letter-spacing: 1px;
margin-right: 10px;
padding-right: 10px;
&:hover {
- color: $blue;
+ color: $link-color;
}
}
@@ -240,9 +240,9 @@
}
&:hover {
- background: rgb(245,245,245);
- border-color: rgb(170,170,170);
- @include box-shadow(0 1px 16px 0 rgba($blue, 0.4));
+ background: $course-profile-bg;
+ border-color: $border-color-1;
+ @include box-shadow(0 1px 16px 0 rgba($shadow-color, 0.4));
.info {
top: -150px;
diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss
index d891ff408b..e3e99ae301 100644
--- a/lms/static/sass/shared/_footer.scss
+++ b/lms/static/sass/shared/_footer.scss
@@ -159,4 +159,4 @@
width: 360px;
}
}
-}
\ No newline at end of file
+}
diff --git a/lms/static/sass/shared/_forms.scss b/lms/static/sass/shared/_forms.scss
index 79d476f420..3350081850 100644
--- a/lms/static/sass/shared/_forms.scss
+++ b/lms/static/sass/shared/_forms.scss
@@ -15,8 +15,8 @@ input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"] {
- background: rgb(250,250,250);
- border: 1px solid rgb(200,200,200);
+ background: $form-bg-color;
+ border: 1px solid $border-color-2;
@include border-radius(3px);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
@include box-sizing(border-box);
@@ -31,8 +31,8 @@ input[type="tel"] {
}
&:focus {
- border-color: lighten($blue, 20%);
- @include box-shadow(0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15));
+ border-color: darken($button-archive-color, 50%);
+ @include box-shadow(0 0 6px 0 darken($button-archive-color, 50%), inset 0 0 4px 0 rgba(0,0,0, 0.15));
outline: none;
}
}
@@ -46,7 +46,7 @@ input[type="button"],
button,
.button {
@include border-radius(3px);
- @include button(shiny, $blue);
+ @include button(shiny, $button-color);
font: normal 1.2rem/1.6rem $sans-serif;
letter-spacing: 1px;
padding: 4px 20px;
diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss
index 5eb453448c..6987b35c84 100644
--- a/lms/static/sass/shared/_header.scss
+++ b/lms/static/sass/shared/_header.scss
@@ -54,8 +54,7 @@ header.global {
li.secondary {
> a {
- color: $lighter-base-font-color;
- color: $blue;
+ color: $link-color;
display: block;
font-family: $sans-serif;
@include inline-block;
@@ -78,9 +77,9 @@ header.global {
margin-right: 5px;
> a {
- @include background-image(linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%));
- border: 1px solid transparent;
- border-color: rgb(200,200,200);
+ @include background-image($button-bg-image);
+ background-color: $button-bg-color;
+ border: 1px solid $border-color-2;
@include border-radius(3px);
@include box-sizing(border-box);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
@@ -101,7 +100,7 @@ header.global {
}
&:hover, &.active {
- background: #FFF;
+ background: $button-bg-hover-color;
}
}
}
@@ -159,10 +158,10 @@ header.global {
}
ul.dropdown-menu {
- background: rgb(252,252,252);
+ background: $border-color-4;
@include border-radius(4px);
@include box-shadow(0 2px 24px 0 rgba(0,0,0, 0.3));
- border: 1px solid rgb(100,100,100);
+ border: 1px solid $border-color-3;
display: none;
padding: 5px 10px;
position: absolute;
@@ -178,12 +177,12 @@ header.global {
&::before {
background: transparent;
border: {
- top: 6px solid rgba(252,252,252, 1);
- right: 6px solid rgba(252,252,252, 1);
+ top: 6px solid $border-color-4;
+ right: 6px solid $border-color-4;
bottom: 6px solid transparent;
left: 6px solid transparent;
}
- @include box-shadow(1px 0 0 0 rgb(0,0,0), 0 -1px 0 0 rgb(0,0,0));
+ @include box-shadow(1px 0 0 0 $border-color-3, 0 -1px 0 0 $border-color-3);
content: "";
display: block;
height: 0px;
@@ -196,7 +195,7 @@ header.global {
li {
display: block;
- border-top: 1px dotted rgba(200,200,200, 1);
+ border-top: 1px dotted $border-color-2;
@include box-shadow(inset 0 1px 0 0 rgba(255,255,255, 0.05));
&:first-child {
@@ -208,7 +207,7 @@ header.global {
border: 1px solid transparent;
@include border-radius(3px);
@include box-sizing(border-box);
- color: $blue;
+ color: $link-color;
cursor: pointer;
display: block;
margin: 5px 0px;
@@ -328,4 +327,4 @@ header.global {
text-decoration: none;
color: $m-blue-s1 !important;
}
-}
\ No newline at end of file
+}
diff --git a/lms/static/sass/shared/_modal.scss b/lms/static/sass/shared/_modal.scss
index 8ff58c1c14..7a51213dee 100644
--- a/lms/static/sass/shared/_modal.scss
+++ b/lms/static/sass/shared/_modal.scss
@@ -52,7 +52,7 @@
}
.inner-wrapper {
- background: rgb(245,245,245);
+ background: $modal-bg-color;
@include border-radius(0px);
border: 1px solid rgba(0, 0, 0, 0.9);
@include box-shadow(inset 0 1px 0 0 rgba(255, 255, 255, 0.7));
@@ -149,7 +149,7 @@
}
label {
- color: #646464;
+ color: $text-color;
&.field-error {
display: block;
diff --git a/lms/templates/email_change_failed.html b/lms/templates/email_change_failed.html
new file mode 100644
index 0000000000..e228df4a9c
--- /dev/null
+++ b/lms/templates/email_change_failed.html
@@ -0,0 +1,3 @@
+
E-mail change failed.
+
+
We were unable to send a confirmation email to ${email}
diff --git a/package.json b/package.json
index 7fa287018a..2dd67d5be4 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"dependencies": {
"coffee-script": "1.6.X",
- "phantom-jasmine": "0.1.0"
+ "phantom-jasmine": "0.1.0",
+ "jasmine-reporters": "0.2.1"
}
}
diff --git a/pylintrc b/pylintrc
index 792079ce03..d4085379b4 100644
--- a/pylintrc
+++ b/pylintrc
@@ -110,7 +110,9 @@ generated-members=
get_url,
size,
content,
- status_code
+ status_code,
+# For factory_body factories
+ create
[BASIC]
diff --git a/rakefiles/jasmine.rake b/rakefiles/jasmine.rake
index 1e5050801e..4182bef9e2 100644
--- a/rakefiles/jasmine.rake
+++ b/rakefiles/jasmine.rake
@@ -48,6 +48,7 @@ def template_jasmine_runner(lib)
sh("node_modules/.bin/coffee -c #{coffee_files.join(' ')}")
end
phantom_jasmine_path = File.expand_path("node_modules/phantom-jasmine")
+ jasmine_reporters_path = File.expand_path("node_modules/jasmine-reporters")
common_js_root = File.expand_path("common/static/js")
common_coffee_root = File.expand_path("common/static/coffee/src")
@@ -58,6 +59,7 @@ def template_jasmine_runner(lib)
js_specs = Dir[spec_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
js_source = Dir[src_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
+ report_dir = report_dir_path("#{lib}/jasmine")
template = ERB.new(File.read("common/templates/jasmine/jasmine_test_runner.html.erb"))
template_output = "#{lib}/jasmine_test_runner.html"
File.open(template_output, 'w') do |f|
@@ -66,6 +68,11 @@ def template_jasmine_runner(lib)
yield File.expand_path(template_output)
end
+def run_phantom_js(url)
+ phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
+ sh("#{phantomjs} node_modules/jasmine-reporters/test/phantomjs-testrunner.js #{url}")
+end
+
[:lms, :cms].each do |system|
desc "Open jasmine tests for #{system} in your default browser"
task "browse_jasmine_#{system}" => :assets do
@@ -78,14 +85,16 @@ end
desc "Use phantomjs to run jasmine tests for #{system} from the console"
task "phantomjs_jasmine_#{system}" => :assets do
- phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
django_for_jasmine(system, false) do |jasmine_url|
- sh("#{phantomjs} node_modules/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}")
+ run_phantom_js(jasmine_url)
end
end
end
-Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
+STATIC_JASMINE_TESTS = Dir["common/lib/*"].select{|lib| File.directory?(lib)}
+STATIC_JASMINE_TESTS << 'common/static/coffee'
+
+STATIC_JASMINE_TESTS.each do |lib|
desc "Open jasmine tests for #{lib} in your default browser"
task "browse_jasmine_#{lib}" do
template_jasmine_runner(lib) do |f|
@@ -97,26 +106,14 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
desc "Use phantomjs to run jasmine tests for #{lib} from the console"
task "phantomjs_jasmine_#{lib}" do
- phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
template_jasmine_runner(lib) do |f|
- sh("#{phantomjs} node_modules/phantom-jasmine/lib/run_jasmine_test.coffee #{f}")
+ run_phantom_js(f)
end
end
end
desc "Open jasmine tests for discussion in your default browser"
-task "browse_jasmine_discussion" do
- template_jasmine_runner("common/static/coffee") do |f|
- sh("python -m webbrowser -t 'file://#{f}'")
- puts "Press ENTER to terminate".red
- $stdin.gets
- end
-end
+task "browse_jasmine_discussion" => "browse_jasmine_common/static/coffee"
desc "Use phantomjs to run jasmine tests for discussion from the console"
-task "phantomjs_jasmine_discussion" do
- phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
- template_jasmine_runner("common/static/coffee") do |f|
- sh("#{phantomjs} node_modules/phantom-jasmine/lib/run_jasmine_test.coffee #{f}")
- end
-end
+task "phantomjs_jasmine_discussion" => "phantomjs_jasmine_common/static/coffee"
diff --git a/rakefiles/prereqs.rake b/rakefiles/prereqs.rake
index f453372065..ff8b4b8784 100644
--- a/rakefiles/prereqs.rake
+++ b/rakefiles/prereqs.rake
@@ -31,6 +31,7 @@ task :install_python_prereqs => "ws:migrate" do
unchanged = 'Python requirements unchanged, nothing to install'
when_changed(unchanged, ['requirements/**/*'], [site_packages_dir]) do
ENV['PIP_DOWNLOAD_CACHE'] ||= '.pip_download_cache'
+ sh('pip install --exists-action w -r requirements/edx/pre.txt')
sh('pip install --exists-action w -r requirements/edx/base.txt')
sh('pip install --exists-action w -r requirements/edx/post.txt')
# requirements/private.txt is used to install our libs as
diff --git a/rakefiles/tests.rake b/rakefiles/tests.rake
index ebe8ea6375..448a482f04 100644
--- a/rakefiles/tests.rake
+++ b/rakefiles/tests.rake
@@ -12,10 +12,11 @@ def run_under_coverage(cmd, root)
return cmd
end
-def run_tests(system, report_dir, stop_on_failure=true)
+def run_tests(system, report_dir, test_id=nil, stop_on_failure=true)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
- cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each)
+ test_id = dirs.join(' ') if test_id.nil? or test_id == ''
+ cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', test_id)
sh(run_under_coverage(cmd, system)) do |ok, res|
if !ok and stop_on_failure
abort "Test failed!"
@@ -25,6 +26,16 @@ def run_tests(system, report_dir, stop_on_failure=true)
end
def run_acceptance_tests(system, report_dir, harvest_args)
+ # HACK: Since now the CMS depends on the existence of some database tables
+ # that used to be in LMS (Role/Permissions for Forums) we need to make
+ # sure the acceptance tests create/migrate the database tables
+ # that are represented in the LMS. We might be able to address this by moving
+ # out the migrations from lms/django_comment_client, but then we'd have to
+ # repair all the existing migrations from the upgrade tables in the DB.
+ if system == :cms
+ sh(django_admin('lms', 'acceptance', 'syncdb', '--noinput'))
+ sh(django_admin('lms', 'acceptance', 'migrate', '--noinput'))
+ end
sh(django_admin(system, 'acceptance', 'syncdb', '--noinput'))
sh(django_admin(system, 'acceptance', 'migrate', '--noinput'))
sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--tag -skip', harvest_args))
@@ -44,13 +55,13 @@ TEST_TASK_DIRS = []
# Per System tasks
desc "Run all django tests on our djangoapps for the #{system}"
- task "test_#{system}", [:stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"]
+ task "test_#{system}", [:test_id, :stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"]
# Have a way to run the tests without running collectstatic -- useful when debugging without
# messing with static files.
- task "fasttest_#{system}", [:stop_on_failure] => [report_dir, :install_prereqs, :predjango] do |t, args|
- args.with_defaults(:stop_on_failure => 'true')
- run_tests(system, report_dir, args.stop_on_failure)
+ task "fasttest_#{system}", [:test_id, :stop_on_failure] => [report_dir, :install_prereqs, :predjango] do |t, args|
+ args.with_defaults(:stop_on_failure => 'true', :test_id => nil)
+ run_tests(system, report_dir, args.test_id, args.stop_on_failure)
end
# Run acceptance tests
@@ -100,7 +111,7 @@ end
task :test do
TEST_TASK_DIRS.each do |dir|
- Rake::Task["test_#{dir}"].invoke(false)
+ Rake::Task["test_#{dir}"].invoke(nil, false)
end
if $failed_tests > 0
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 3d8b95f8e2..01768bcac9 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -29,7 +29,6 @@ mako==0.7.3
Markdown==2.2.1
networkx==1.7
nltk==2.0.4
-numpy==1.6.2
paramiko==1.9.0
path.py==3.0.1
Pillow==1.7.8
@@ -43,6 +42,7 @@ python-openid==2.2.5
pytz==2012h
PyYAML==3.10
requests==0.14.2
+scipy==0.11.0
Shapely==1.2.16
sorl-thumbnail==11.12
South==0.7.6
@@ -71,7 +71,7 @@ transifex-client==0.8
coverage==3.6
factory_boy==2.0.2
lettuce==0.2.16
-mock==0.8.0
+mock==1.0.1
nosexcover==1.0.7
pep8==1.4.5
pylint==0.28
@@ -82,3 +82,5 @@ django_nose==1.1
django-jasmine==0.3.2
django_debug_toolbar
django-debug-toolbar-mongo
+
+git+https://github.com/mfogel/django-settings-context-processor.git
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index 6b28d3edd9..f280d66557 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -9,4 +9,4 @@
# Our libraries:
-e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock
--e git+https://github.com/edx/codejail.git@07494f1#egg=codejail
+-e git+https://github.com/edx/codejail.git@72cf791#egg=codejail
diff --git a/requirements/edx/post.txt b/requirements/edx/post.txt
index e1e26b381a..b637b65db0 100644
--- a/requirements/edx/post.txt
+++ b/requirements/edx/post.txt
@@ -1,6 +1,2 @@
-
-# This must be installed after distribute 0.6.28
-MySQL-python==1.2.4c1
-
-# This must be installed after numpy
-scipy==0.11.0
+# This must be installed after distribute has been updated.
+MySQL-python==1.2.4
diff --git a/requirements/edx/pre.txt b/requirements/edx/pre.txt
new file mode 100644
index 0000000000..a8dff9bf9a
--- /dev/null
+++ b/requirements/edx/pre.txt
@@ -0,0 +1,3 @@
+# Numpy and scipy can't be installed in the same pip run.
+# Install numpy before other things to help resolve the problem.
+numpy==1.6.2