diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..72ec77d0e2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "askbot"] + path = askbot + url = git@github.com:MITx/askbot-devel.git diff --git a/askbot b/askbot new file mode 160000 index 0000000000..1c3381046c --- /dev/null +++ b/askbot @@ -0,0 +1 @@ +Subproject commit 1c3381046c78e055439ba1c78e0df48410fcc13e diff --git a/cms/djangoapps/contentstore/__init__.py b/cms/djangoapps/contentstore/__init__.py deleted file mode 100644 index ef4e31614e..0000000000 --- a/cms/djangoapps/contentstore/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.xml import XMLModuleStore -import logging - -log = logging.getLogger(__name__) - - -def import_from_xml(data_dir, course_dirs=None): - """ - Import the specified xml data_dir into the django defined modulestore, - using org and course as the location org and course. - """ - module_store = XMLModuleStore( - data_dir, - default_class='xmodule.raw_module.RawDescriptor', - eager=True, - course_dirs=course_dirs - ) - for module in module_store.modules.itervalues(): - - # TODO (cpennington): This forces import to overrite the same items. - # This should in the future create new revisions of the items on import - try: - modulestore().create_item(module.location) - except: - log.exception('Item already exists at %s' % module.location.url()) - pass - if 'data' in module.definition: - modulestore().update_item(module.location, module.definition['data']) - if 'children' in module.definition: - modulestore().update_children(module.location, module.definition['children']) - modulestore().update_metadata(module.location, dict(module.metadata)) - - return module_store diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 75d4e2618c..69aaa35a7d 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -1,9 +1,11 @@ ### -### One-off script for importing courseware form XML format +### Script for importing courseware from XML format ### from django.core.management.base import BaseCommand, CommandError -from contentstore import import_from_xml +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.django import modulestore + unnamed_modules = 0 @@ -21,4 +23,4 @@ class Command(BaseCommand): course_dirs = args[1:] else: course_dirs = None - import_from_xml(data_dir, course_dirs) + import_from_xml(modulestore(), data_dir, course_dirs) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index b7c55389b6..429774c91e 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -12,7 +12,7 @@ from django.contrib.auth.models import User from xmodule.modulestore.django import modulestore import xmodule.modulestore.django from xmodule.modulestore import Location -from contentstore import import_from_xml +from xmodule.modulestore.xml_importer import import_from_xml import copy @@ -74,7 +74,7 @@ class ContentStoreTestCase(TestCase): return resp def _activate_user(self, email): - '''look up the user's activation key in the db, then hit the activate view. + '''Look up the activation key for the user, then hit the activate view. No error checking''' activation_key = registration(email).activation_key @@ -102,7 +102,7 @@ class AuthTestCase(ContentStoreTestCase): resp = self.client.get(url) self.assertEqual(resp.status_code, expected) return resp - + def test_public_pages_load(self): """Make sure pages that don't require login load without error.""" pages = ( @@ -196,7 +196,7 @@ class EditTestCase(ContentStoreTestCase): xmodule.modulestore.django.modulestore().collection.drop() def check_edit_item(self, test_course_name): - import_from_xml('common/test/data/', test_course_name) + import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) for descriptor in modulestore().get_items(Location(None, None, None, None, None)): print "Checking ", descriptor.location.url() diff --git a/cms/djangoapps/github_sync/__init__.py b/cms/djangoapps/github_sync/__init__.py index 149b92670a..e3215cbec1 100644 --- a/cms/djangoapps/github_sync/__init__.py +++ b/cms/djangoapps/github_sync/__init__.py @@ -5,35 +5,53 @@ from django.conf import settings from fs.osfs import OSFS from git import Repo, PushInfo -from contentstore import import_from_xml -from xmodule.modulestore import Location +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.django import modulestore +from collections import namedtuple -from .exceptions import GithubSyncError +from .exceptions import GithubSyncError, InvalidRepo log = logging.getLogger(__name__) +RepoSettings = namedtuple('RepoSettings', 'path branch origin') + + +def sync_all_with_github(): + """ + Sync all defined repositories from github + """ + for repo_name in settings.REPOS: + sync_with_github(load_repo_settings(repo_name)) + + +def sync_with_github(repo_settings): + """ + Sync specified repository from github + + repo_settings: A RepoSettings defining which repo to sync + """ + revision, course = import_from_github(repo_settings) + export_to_github(course, "Changes from cms import of revision %s" % revision, "CMS ") + def setup_repo(repo_settings): """ Reset the local github repo specified by repo_settings - repo_settings is a dictionary with the following keys: - path: file system path to the local git repo - branch: name of the branch to track on github - origin: git url for the repository to track + repo_settings (RepoSettings): The settings for the repo to reset """ - course_dir = repo_settings['path'] + course_dir = repo_settings.path repo_path = settings.GITHUB_REPO_ROOT / course_dir if not os.path.isdir(repo_path): - Repo.clone_from(repo_settings['origin'], repo_path) + Repo.clone_from(repo_settings.origin, repo_path) git_repo = Repo(repo_path) origin = git_repo.remotes.origin origin.fetch() # Do a hard reset to the remote branch so that we have a clean import - git_repo.git.checkout(repo_settings['branch']) + git_repo.git.checkout(repo_settings.branch) return git_repo @@ -42,21 +60,22 @@ def load_repo_settings(course_dir): """ Returns the repo_settings for the course stored in course_dir """ - for repo_settings in settings.REPOS.values(): - if repo_settings['path'] == course_dir: - return repo_settings - raise InvalidRepo(course_dir) + if course_dir not in settings.REPOS: + raise InvalidRepo(course_dir) + + return RepoSettings(course_dir, **settings.REPOS[course_dir]) def import_from_github(repo_settings): """ Imports data into the modulestore based on the XML stored on github """ - course_dir = repo_settings['path'] + course_dir = repo_settings.path git_repo = setup_repo(repo_settings) - git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True) + git_repo.head.reset('origin/%s' % repo_settings.branch, index=True, working_tree=True) - module_store = import_from_xml(settings.GITHUB_REPO_ROOT, course_dirs=[course_dir]) + module_store = import_from_xml(modulestore(), + settings.GITHUB_REPO_ROOT, course_dirs=[course_dir]) return git_repo.head.commit.hexsha, module_store.courses[course_dir] diff --git a/cms/djangoapps/github_sync/exceptions.py b/cms/djangoapps/github_sync/exceptions.py index 9097ffc2a6..1fe8d1d73e 100644 --- a/cms/djangoapps/github_sync/exceptions.py +++ b/cms/djangoapps/github_sync/exceptions.py @@ -1,2 +1,6 @@ class GithubSyncError(Exception): pass + + +class InvalidRepo(Exception): + pass diff --git a/cms/djangoapps/github_sync/management/__init__.py b/cms/djangoapps/github_sync/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/github_sync/management/commands/__init__.py b/cms/djangoapps/github_sync/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/github_sync/management/commands/sync_with_github.py b/cms/djangoapps/github_sync/management/commands/sync_with_github.py new file mode 100644 index 0000000000..4383871df3 --- /dev/null +++ b/cms/djangoapps/github_sync/management/commands/sync_with_github.py @@ -0,0 +1,14 @@ +### +### Script for syncing CMS with defined github repos +### + +from django.core.management.base import NoArgsCommand +from github_sync import sync_all_with_github + + +class Command(NoArgsCommand): + help = \ +'''Sync the CMS with the defined github repos''' + + def handle_noargs(self, **options): + sync_all_with_github() diff --git a/cms/djangoapps/github_sync/tests/__init__.py b/cms/djangoapps/github_sync/tests/__init__.py index c95d538030..581ac3cb25 100644 --- a/cms/djangoapps/github_sync/tests/__init__.py +++ b/cms/djangoapps/github_sync/tests/__init__.py @@ -1,14 +1,17 @@ from django.test import TestCase from path import path import shutil -import os -from github_sync import import_from_github, export_to_github +from github_sync import ( + import_from_github, export_to_github, load_repo_settings, + sync_all_with_github, sync_with_github +) from git import Repo from django.conf import settings from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from override_settings import override_settings from github_sync.exceptions import GithubSyncError +from mock import patch, Mock REPO_DIR = settings.GITHUB_REPO_ROOT / 'local_repo' WORKING_DIR = path(settings.TEST_ROOT) @@ -16,8 +19,7 @@ REMOTE_DIR = WORKING_DIR / 'remote_repo' @override_settings(REPOS={ - 'local': { - 'path': 'local_repo', + 'local_repo': { 'origin': REMOTE_DIR, 'branch': 'master', } @@ -40,7 +42,7 @@ class GithubSyncTestCase(TestCase): remote.git.commit(m='Initial commit') remote.git.config("receive.denyCurrentBranch", "ignore") - self.import_revision, self.import_course = import_from_github(settings.REPOS['local']) + self.import_revision, self.import_course = import_from_github(load_repo_settings('local_repo')) def tearDown(self): self.cleanup() @@ -57,10 +59,23 @@ class GithubSyncTestCase(TestCase): """ self.assertEquals('Toy Course', self.import_course.metadata['display_name']) self.assertIn( - Location('i4x://edx/local_repo/chapter/Overview'), + Location('i4x://edX/toy/chapter/Overview'), [child.location for child in self.import_course.get_children()]) self.assertEquals(1, len(self.import_course.get_children())) + @patch('github_sync.sync_with_github') + def test_sync_all_with_github(self, sync_with_github): + sync_all_with_github() + sync_with_github.assert_called_with(load_repo_settings('local_repo')) + + def test_sync_with_github(self): + with patch('github_sync.import_from_github', Mock(return_value=(Mock(), Mock()))) as import_from_github: + with patch('github_sync.export_to_github') as export_to_github: + settings = load_repo_settings('local_repo') + sync_with_github(settings) + import_from_github.assert_called_with(settings) + export_to_github.assert_called + @override_settings(MITX_FEATURES={'GITHUB_PUSH': False}) def test_export_no_pash(self): """ diff --git a/cms/djangoapps/github_sync/tests/test_views.py b/cms/djangoapps/github_sync/tests/test_views.py index f46e7f7db3..212d707340 100644 --- a/cms/djangoapps/github_sync/tests/test_views.py +++ b/cms/djangoapps/github_sync/tests/test_views.py @@ -1,52 +1,43 @@ import json from django.test.client import Client from django.test import TestCase -from mock import patch, Mock +from mock import patch from override_settings import override_settings -from django.conf import settings +from github_sync import load_repo_settings -@override_settings(REPOS={'repo': {'path': 'path', 'branch': 'branch'}}) +@override_settings(REPOS={'repo': {'branch': 'branch', 'origin': 'origin'}}) class PostReceiveTestCase(TestCase): def setUp(self): self.client = Client() - @patch('github_sync.views.export_to_github') - @patch('github_sync.views.import_from_github') - def test_non_branch(self, import_from_github, export_to_github): + @patch('github_sync.views.sync_with_github') + def test_non_branch(self, sync_with_github): self.client.post('/github_service_hook', {'payload': json.dumps({ 'ref': 'refs/tags/foo'}) }) - self.assertFalse(import_from_github.called) - self.assertFalse(export_to_github.called) + self.assertFalse(sync_with_github.called) - @patch('github_sync.views.export_to_github') - @patch('github_sync.views.import_from_github') - def test_non_watched_repo(self, import_from_github, export_to_github): + @patch('github_sync.views.sync_with_github') + def test_non_watched_repo(self, sync_with_github): self.client.post('/github_service_hook', {'payload': json.dumps({ 'ref': 'refs/heads/branch', 'repository': {'name': 'bad_repo'}}) }) - self.assertFalse(import_from_github.called) - self.assertFalse(export_to_github.called) + self.assertFalse(sync_with_github.called) - @patch('github_sync.views.export_to_github') - @patch('github_sync.views.import_from_github') - def test_non_tracked_branch(self, import_from_github, export_to_github): + @patch('github_sync.views.sync_with_github') + def test_non_tracked_branch(self, sync_with_github): self.client.post('/github_service_hook', {'payload': json.dumps({ 'ref': 'refs/heads/non_branch', 'repository': {'name': 'repo'}}) }) - self.assertFalse(import_from_github.called) - self.assertFalse(export_to_github.called) + self.assertFalse(sync_with_github.called) - @patch('github_sync.views.export_to_github') - @patch('github_sync.views.import_from_github', return_value=(Mock(), Mock())) - def test_tracked_branch(self, import_from_github, export_to_github): + @patch('github_sync.views.sync_with_github') + def test_tracked_branch(self, sync_with_github): self.client.post('/github_service_hook', {'payload': json.dumps({ 'ref': 'refs/heads/branch', 'repository': {'name': 'repo'}}) }) - import_from_github.assert_called_with(settings.REPOS['repo']) - mock_revision, mock_course = import_from_github.return_value - export_to_github.assert_called_with(mock_course, 'path', "Changes from cms import of revision %s" % mock_revision) + sync_with_github.assert_called_with(load_repo_settings('repo')) diff --git a/cms/djangoapps/github_sync/views.py b/cms/djangoapps/github_sync/views.py index e4cae6cad8..941d50f986 100644 --- a/cms/djangoapps/github_sync/views.py +++ b/cms/djangoapps/github_sync/views.py @@ -5,7 +5,7 @@ from django.http import HttpResponse from django.conf import settings from django_future.csrf import csrf_exempt -from . import import_from_github, export_to_github +from . import sync_with_github, load_repo_settings log = logging.getLogger() @@ -40,13 +40,12 @@ def github_post_receive(request): log.info('No repository matching %s found' % repo_name) return HttpResponse('No Repo Found') - repo = settings.REPOS[repo_name] + repo = load_repo_settings(repo_name) - if repo['branch'] != branch_name: + if repo.branch != branch_name: log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name)) return HttpResponse('Ignoring non-tracked branch') - revision, course = import_from_github(repo) - export_to_github(course, repo['path'], "Changes from cms import of revision %s" % revision) + sync_with_github(repo) return HttpResponse('Push received') diff --git a/cms/envs/aws.py b/cms/envs/aws.py new file mode 100644 index 0000000000..f03e10c9b1 --- /dev/null +++ b/cms/envs/aws.py @@ -0,0 +1,48 @@ +""" +This is the default template for our main set of AWS servers. +""" +import json + +from .logsettings import get_logger_config +from .common import * + +############################### ALWAYS THE SAME ################################ +DEBUG = False +TEMPLATE_DEBUG = False + +EMAIL_BACKEND = 'django_ses.SESBackend' +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' +DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' + +########################### NON-SECURE ENV CONFIG ############################## +# Things like server locations, ports, etc. +with open(ENV_ROOT / "cms.env.json") as env_file: + ENV_TOKENS = json.load(env_file) + +SITE_NAME = ENV_TOKENS['SITE_NAME'] + +LOG_DIR = ENV_TOKENS['LOG_DIR'] + +CACHES = ENV_TOKENS['CACHES'] + +for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items(): + MITX_FEATURES[feature] = value + +LOGGING = get_logger_config(LOG_DIR, + logging_env=ENV_TOKENS['LOGGING_ENV'], + syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), + debug=False) + +with open(ENV_ROOT / "repos.json") as repos_file: + REPOS = json.load(repos_file) + + +############################## SECURE AUTH ITEMS ############################### +# Secret things: passwords, access keys, etc. +with open(ENV_ROOT / "cms.auth.json") as auth_file: + AUTH_TOKENS = json.load(auth_file) + +AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"] +AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"] +DATABASES = AUTH_TOKENS['DATABASES'] +MODULESTORE = AUTH_TOKENS['MODULESTORE'] diff --git a/cms/envs/common.py b/cms/envs/common.py index 582cb75abf..3f8f4440c5 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -243,7 +243,7 @@ with open(module_styles_path, 'w') as module_styles: PIPELINE_CSS = { 'base-style': { 'source_filenames': ['sass/base-style.scss'], - 'output_filename': 'css/base-style.css', + 'output_filename': 'css/cms-base-style.css', }, } @@ -260,15 +260,15 @@ PIPELINE_JS = { for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee') ], - 'output_filename': 'js/application.js', + 'output_filename': 'js/cms-application.js', }, 'module-js': { 'source_filenames': module_js_sources, - 'output_filename': 'js/modules.js', + 'output_filename': 'js/cms-modules.js', }, 'spec': { 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')], - 'output_filename': 'js/spec.js' + 'output_filename': 'js/cms-spec.js' } } @@ -309,6 +309,7 @@ INSTALLED_APPS = ( # For CMS 'contentstore', + 'github_sync', 'student', # misleading name due to sharing with lms # For asset pipelining diff --git a/cms/envs/dev.py b/cms/envs/dev.py index dd12ce5770..b0729ba885 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -32,38 +32,23 @@ DATABASES = { REPOS = { 'edx4edx': { - 'path': "edx4edx", - 'org': 'edx', - 'course': 'edx4edx', - 'branch': 'for_cms', + 'branch': 'master', 'origin': 'git@github.com:MITx/edx4edx.git', }, - '6002x-fall-2012': { - 'path': '6002x-fall-2012', - 'org': 'mit.edu', - 'course': '6.002x', - 'branch': 'for_cms', + 'content-mit-6002x': { + 'branch': 'master', 'origin': 'git@github.com:MITx/6002x-fall-2012.git', }, '6.00x': { - 'path': '6.00x', - 'org': 'mit.edu', - 'course': '6.00x', - 'branch': 'for_cms', + 'branch': 'master', 'origin': 'git@github.com:MITx/6.00x.git', }, '7.00x': { - 'path': '7.00x', - 'org': 'mit.edu', - 'course': '7.00x', - 'branch': 'for_cms', + 'branch': 'master', 'origin': 'git@github.com:MITx/7.00x.git', }, '3.091x': { - 'path': '3.091x', - 'org': 'mit.edu', - 'course': '3.091x', - 'branch': 'for_cms', + 'branch': 'master', 'origin': 'git@github.com:MITx/3.091x.git', }, } diff --git a/cms/envs/logsettings.py b/cms/envs/logsettings.py new file mode 100644 index 0000000000..31130e33c6 --- /dev/null +++ b/cms/envs/logsettings.py @@ -0,0 +1,95 @@ +import os +import os.path +import platform +import sys + +def get_logger_config(log_dir, + logging_env="no_env", + tracking_filename=None, + syslog_addr=None, + debug=False): + """Return the appropriate logging config dictionary. You should assign the + result of this to the LOGGING var in your settings. The reason it's done + this way instead of registering directly is because I didn't want to worry + about resetting the logging state if this is called multiple times when + settings are extended.""" + + # If we're given an explicit place to put tracking logs, we do that (say for + # debugging). However, logging is not safe for multiple processes hitting + # the same file. So if it's left blank, we dynamically create the filename + # based on the PID of this worker process. + if tracking_filename: + tracking_file_loc = os.path.join(log_dir, tracking_filename) + else: + pid = os.getpid() # So we can log which process is creating the log + tracking_file_loc = os.path.join(log_dir, "tracking_{0}.log".format(pid)) + + hostname = platform.node().split(".")[0] + syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s [{hostname} " + + " %(process)d] [%(filename)s:%(lineno)d] - %(message)s").format( + logging_env=logging_env, hostname=hostname) + + handlers = ['console'] if debug else ['console', 'syslogger', 'newrelic'] + + return { + 'version': 1, + 'formatters' : { + 'standard' : { + 'format' : '%(asctime)s %(levelname)s %(process)d [%(name)s] %(filename)s:%(lineno)d - %(message)s', + }, + 'syslog_format' : { 'format' : syslog_format }, + 'raw' : { 'format' : '%(message)s' }, + }, + 'handlers' : { + 'console' : { + 'level' : 'DEBUG' if debug else 'INFO', + 'class' : 'logging.StreamHandler', + 'formatter' : 'standard', + 'stream' : sys.stdout, + }, + 'syslogger' : { + 'level' : 'INFO', + 'class' : 'logging.handlers.SysLogHandler', + 'address' : syslog_addr, + 'formatter' : 'syslog_format', + }, + 'tracking' : { + 'level' : 'DEBUG', + 'class' : 'logging.handlers.WatchedFileHandler', + 'filename' : tracking_file_loc, + 'formatter' : 'raw', + }, + 'newrelic' : { + 'level': 'ERROR', + 'class': 'newrelic_logging.NewRelicHandler', + 'formatter': 'raw', + } + }, + 'loggers' : { + 'django' : { + 'handlers' : handlers, + 'propagate' : True, + 'level' : 'INFO' + }, + 'tracking' : { + 'handlers' : ['tracking'], + 'level' : 'DEBUG', + 'propagate' : False, + }, + '' : { + 'handlers' : handlers, + 'level' : 'DEBUG', + 'propagate' : False + }, + 'mitx' : { + 'handlers' : handlers, + 'level' : 'DEBUG', + 'propagate' : False + }, + 'keyedcache' : { + 'handlers' : handlers, + 'level' : 'DEBUG', + 'propagate' : False + }, + } + } diff --git a/cms/static/img/content-types/module.png b/cms/static/img/content-types/module.png new file mode 100644 index 0000000000..643c12d1d9 Binary files /dev/null and b/cms/static/img/content-types/module.png differ diff --git a/cms/static/img/menu.png b/cms/static/img/menu.png new file mode 100644 index 0000000000..7449b34c1c Binary files /dev/null and b/cms/static/img/menu.png differ diff --git a/cms/static/img/noise.png b/cms/static/img/noise.png new file mode 100644 index 0000000000..8fa5168293 Binary files /dev/null and b/cms/static/img/noise.png differ diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 05014dbb7e..cad315f6e4 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -5,7 +5,7 @@ $body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida $body-font-size: 14px; $body-line-height: 20px; -$light-blue: #f0f8fa; +$light-blue: #f0f7fd; $dark-blue: #50545c; $bright-blue: #3c8ebf; $orange: #f96e5b; @@ -13,6 +13,14 @@ $yellow: #fff8af; $cream: #F6EFD4; $mit-red: #933; +@mixin hide-text { + background-color: transparent; + border: 0; + color: transparent; + font: 0/0 a; + text-shadow: none; +} + // Base html styles html { height: 100%; @@ -34,14 +42,18 @@ input { button, input[type="submit"], .button { background-color: $orange; - border: 0; + border: 1px solid darken($orange, 15%); + @include border-radius(4px); + @include box-shadow(inset 0 0 0 1px adjust-hue($orange, 20%), 0 1px 0 #fff); color: #fff; font-weight: bold; - padding: 8px 10px; + @include linear-gradient(adjust-hue($orange, 8%), $orange); + padding: 6px 20px; + text-shadow: 0 1px 0 darken($orange, 10%); -webkit-font-smoothing: antialiased; - &:hover { - background-color: shade($orange, 10%); + &:hover, &:focus { + @include box-shadow(inset 0 0 6px 1px adjust-hue($orange, 30%)); } } @@ -122,10 +134,10 @@ textarea { } } -.wip { - outline: 1px solid #f00 !important; - position: relative; -} +// .wip { +// outline: 1px solid #f00 !important; +// position: relative; +// } .hidden { display: none; diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss index 4070766617..35609b2d56 100644 --- a/cms/static/sass/_calendar.scss +++ b/cms/static/sass/_calendar.scss @@ -1,13 +1,15 @@ section.cal { @include box-sizing(border-box); @include clearfix; - padding: 25px; + padding: 20px; > header { + display: none; @include clearfix; margin-bottom: 10px; opacity: .4; @include transition; + text-shadow: 0 1px 0 #fff; &:hover { opacity: 1; @@ -70,12 +72,15 @@ section.cal { ol { list-style: none; @include clearfix; - border-left: 1px solid lighten($dark-blue, 40%); - border-top: 1px solid lighten($dark-blue, 40%); + border: 1px solid lighten( $dark-blue , 30% ); + background: #FFF; width: 100%; @include box-sizing(border-box); margin: 0; padding: 0; + @include box-shadow(0 0 5px lighten($dark-blue, 45%)); + @include border-radius(3px); + overflow: hidden; > li { border-right: 1px solid lighten($dark-blue, 40%); @@ -84,6 +89,7 @@ section.cal { float: left; width: flex-grid(3) + ((flex-gutter() * 3) / 4); background-color: $light-blue; + @include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%)); &:hover { li.create-module { @@ -91,6 +97,10 @@ section.cal { } } + &:nth-child(4n) { + border-right: 0; + } + header { border-bottom: 1px solid lighten($dark-blue, 40%); @include box-shadow(0 2px 2px $light-blue); @@ -128,6 +138,7 @@ section.cal { color: #888; border-bottom: 0; font-size: 12px; + @include box-shadow(none); } } } @@ -138,9 +149,11 @@ section.cal { padding: 0; li { - border-bottom: 1px solid darken($light-blue, 8%); - position: relative; + border-bottom: 1px solid darken($light-blue, 6%); + // @include box-shadow(0 1px 0 lighten($light-blue, 4%)); overflow: hidden; + position: relative; + text-shadow: 0 1px 0 #fff; &:hover { background-color: lighten($yellow, 14%); @@ -314,16 +327,13 @@ section.cal { @include box-sizing(border-box); opacity: .4; @include transition(); - background: darken($light-blue, 2%); &:hover { opacity: 1; width: flex-grid(5) + flex-gutter(); - background-color: transparent; + section.main-content { width: flex-grid(7); - opacity: .6; } } @@ -340,6 +350,7 @@ section.cal { display: block; li { + ul { display: inline; } @@ -351,6 +362,7 @@ section.cal { li { @include box-sizing(border-box); width: 100%; + border-right: 0; &.create-module { display: none; diff --git a/cms/static/sass/_content-types.scss b/cms/static/sass/_content-types.scss index 587646fb39..00af06d5ad 100644 --- a/cms/static/sass/_content-types.scss +++ b/cms/static/sass/_content-types.scss @@ -53,3 +53,13 @@ @extend .content-type; background-image: url('../img/content-types/chapter.png'); } + +.module a:first-child { + @extend .content-type; + background-image: url('/static/img/content-types/module.png'); +} + +.module a:first-child { + @extend .content-type; + background-image: url('/static/img/content-types/module.png'); +} diff --git a/cms/static/sass/_index.scss b/cms/static/sass/_index.scss new file mode 100644 index 0000000000..a3e210b558 --- /dev/null +++ b/cms/static/sass/_index.scss @@ -0,0 +1,80 @@ +body.index { + > header { + display: none; + } + + > h1 { + font-weight: 300; + color: lighten($dark-blue, 40%); + text-shadow: 0 1px 0 #fff; + -webkit-font-smoothing: antialiased; + max-width: 600px; + text-align: center; + margin: 80px auto 30px; + } + + section.main-container { + border-right: 3px; + background: #FFF; + max-width: 600px; + margin: 0 auto; + display: block; + @include box-sizing(border-box); + border: 1px solid lighten( $dark-blue , 30% ); + @include border-radius(3px); + overflow: hidden; + @include bounce-in-animation(.8s); + + header { + border-bottom: 1px solid lighten($dark-blue, 50%); + @include linear-gradient(#fff, lighten($dark-blue, 62%)); + @include clearfix(); + @include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff); + text-shadow: 0 1px 0 #fff; + + h1 { + font-size: 14px; + padding: 8px 20px; + float: left; + color: $dark-blue; + margin: 0; + } + + a { + float: right; + padding: 8px 20px; + border-left: 1px solid lighten($dark-blue, 50%); + @include box-shadow( inset -1px 0 0 #fff); + font-weight: bold; + font-size: 22px; + line-height: 1; + color: $dark-blue; + } + } + + ol { + list-style: none; + margin: 0; + padding: 0; + + li { + border-bottom: 1px solid lighten($dark-blue, 50%); + + a { + display: block; + padding: 10px 20px; + + &:hover { + color: $dark-blue; + background: lighten($yellow, 10%); + text-shadow: 0 1px 0 #fff; + } + } + + &:last-child { + border-bottom: none; + } + } + } + } +} diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/_keyframes.scss new file mode 100644 index 0000000000..7661f18980 --- /dev/null +++ b/cms/static/sass/_keyframes.scss @@ -0,0 +1,27 @@ +@mixin bounce-in { + 0% { + opacity: 0; + @include transform(scale(.3)); + } + + 50% { + opacity: 1; + @include transform(scale(1.05)); + } + + 100% { + @include transform(scale(1)); + } +} + +@-moz-keyframes bounce-in { @include bounce-in(); } +@-webkit-keyframes bounce-in { @include bounce-in(); } +@-o-keyframes bounce-in { @include bounce-in(); } +@keyframes bounce-in { @include bounce-in();} + +@mixin bounce-in-animation($duration, $timing: ease-in-out) { + @include animation-name(bounce-in); + @include animation-duration($duration); + @include animation-timing-function($timing); + @include animation-fill-mode(both); +} diff --git a/cms/static/sass/_layout.scss b/cms/static/sass/_layout.scss index f4c9f63ea6..43308a973c 100644 --- a/cms/static/sass/_layout.scss +++ b/cms/static/sass/_layout.scss @@ -2,6 +2,8 @@ body { @include clearfix(); height: 100%; font: 14px $body-font-family; + background-color: lighten($dark-blue, 62%); + background-image: url('/static/img/noise.png'); > section { display: table; @@ -11,28 +13,53 @@ body { > header { background: $dark-blue; + @include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue)); + border-bottom: 1px solid darken($dark-blue, 15%); + @include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%)); + @include box-sizing(border-box); color: #fff; display: block; float: none; - padding: 8px 25px; + padding: 0 20px; + text-shadow: 0 -1px 0 darken($dark-blue, 15%); width: 100%; - @include box-sizing(border-box); - -webkit-font-smoothing: antialiased; nav { @include clearfix; - h2 { - font-size: 14px; - text-transform: uppercase; + > a { + @include hide-text; + background: url('/static/img/menu.png') 0 center no-repeat; + border-right: 1px solid darken($dark-blue, 10%); + @include box-shadow(1px 0 0 lighten($dark-blue, 10%)); + display: block; float: left; - margin: 0 15px 0 0; + height: 19px; + padding: 8px 10px 8px 0; + width: 14px; + + &:hover, &:focus { + opacity: .7; + } + } + + h2 { + border-right: 1px solid darken($dark-blue, 10%); + @include box-shadow(1px 0 0 lighten($dark-blue, 10%)); + float: left; + font-size: 14px; + margin: 0; + text-transform: uppercase; + -webkit-font-smoothing: antialiased; a { color: #fff; + padding: 8px 20px; + display: block; &:hover { - color: rgba(#fff, .6); + background-color: rgba(darken($dark-blue, 15%), .5); + color: $yellow; } } } @@ -48,21 +75,35 @@ body { ul { float: left; margin: 0; + padding: 0; + @include clearfix; &.user-nav { float: right; + border-left: 1px solid darken($dark-blue, 10%); } li { - @include inline-block(); + border-right: 1px solid darken($dark-blue, 10%); + float: left; + @include box-shadow(1px 0 0 lighten($dark-blue, 10%)); a { - padding: 8px 10px; + padding: 8px 20px; display: block; - margin: -8px 0; &:hover { - background-color: darken($dark-blue, 15%); + background-color: rgba(darken($dark-blue, 15%), .5); + color: $yellow; + } + + &.new-module { + &:before { + @include inline-block; + content: "+"; + font-weight: bold; + margin-right: 10px; + } } } } @@ -76,8 +117,9 @@ body { @include box-sizing(border-box); width: flex-grid(9) + flex-gutter(); float: left; - @include box-shadow( -2px 0 0 darken($light-blue, 3%)); + @include box-shadow( -2px 0 0 lighten($dark-blue, 55%)); @include transition(); + background: #FFF; } } } diff --git a/cms/static/sass/_section.scss b/cms/static/sass/_section.scss index fa08e02901..97818326be 100644 --- a/cms/static/sass/_section.scss +++ b/cms/static/sass/_section.scss @@ -1,6 +1,7 @@ section#unit-wrapper { section.filters { @include clearfix; + display: none; opacity: .4; margin-bottom: 10px; @include transition; @@ -52,22 +53,22 @@ section#unit-wrapper { display: table; border: 1px solid lighten($dark-blue, 40%); width: 100%; + @include border-radius(3px); + @include box-shadow(0 0 4px lighten($dark-blue, 50%)); section { header { background: #fff; padding: 6px; border-bottom: 1px solid lighten($dark-blue, 60%); - border-top: 1px solid lighten($dark-blue, 60%); - margin-top: -1px; @include clearfix; h2 { color: $bright-blue; - float: left; - font-size: 12px; + // float: left; + font-size: 14px; letter-spacing: 1px; - line-height: 19px; + // line-height: 20px; text-transform: uppercase; margin: 0; } @@ -172,7 +173,6 @@ section#unit-wrapper { padding: 0; li { - border-bottom: 1px solid darken($light-blue, 8%); background: $light-blue; &:last-child { @@ -181,6 +181,7 @@ section#unit-wrapper { &.new-module a { background-color: darken($light-blue, 2%); + border-bottom: 1px solid darken($light-blue, 8%); &:hover { background-color: lighten($yellow, 10%); @@ -199,6 +200,7 @@ section#unit-wrapper { li { padding: 6px; border-collapse: collapse; + border-bottom: 1px solid darken($light-blue, 8%); position: relative; &:last-child { diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index d8613be3c2..0ab0d1064d 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -1,19 +1,19 @@ section#unit-wrapper { > header { - border-bottom: 2px solid $dark-blue; + border-bottom: 1px solid lighten($dark-blue, 50%); + @include linear-gradient(#fff, lighten($dark-blue, 62%)); @include clearfix(); - @include box-shadow( 0 2px 0 darken($light-blue, 3%)); - padding: 6px 20px; + @include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff); + text-shadow: 0 1px 0 #fff; section { float: left; + padding: 10px 20px; h1 { - font-size: 16px; - text-transform: uppercase; - letter-spacing: 1px; + font-size: 18px; @include inline-block(); - color: $bright-blue; + color: $dark-blue; margin: 0; } @@ -22,32 +22,41 @@ section#unit-wrapper { margin: 0; a { - text-indent: -9999px; @include inline-block(); - width: 1px; - height: 100%; + font-size: 12px; } } } div { - float: right; + @include clearfix; color: #666; + float: right; + padding: 6px 20px; a { - display: block; @include inline-block; - &.cancel { - margin-right: 20px; - font-style: italic; - font-size: 12px; - } + &.cancel { + margin-right: 20px; + font-style: italic; + font-size: 12px; + padding: 6px 0; + } - &.save-update { - @extend .button; - margin: -6px -21px -6px 0; - } + &.save-update { + padding: 6px 20px; + @include border-radius(3px); + border: 1px solid lighten($dark-blue, 40%); + @include box-shadow(inset 0 0 0 1px #fff); + color: $dark-blue; + @include linear-gradient(lighten($dark-blue, 60%), lighten($dark-blue, 55%)); + + &:hover, &:focus { + @include linear-gradient(lighten($dark-blue, 58%), lighten($dark-blue, 53%)); + @include box-shadow(inset 0 0 6px 1px #fff); + } + } } } } diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 35c02a4758..49a51a59fb 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -1,8 +1,9 @@ @import 'bourbon/bourbon'; @import 'vendor/normalize'; +@import 'keyframes'; @import 'base', 'layout', 'content-types'; @import 'calendar'; -@import 'section', 'unit'; +@import 'section', 'unit', 'index'; @import 'module/module-styles.scss'; diff --git a/cms/templates/base.html b/cms/templates/base.html index 96f881421a..dba7df95b9 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -15,7 +15,7 @@ - + <%include file="widgets/header.html"/> <%include file="courseware_vendor_js.html"/> diff --git a/cms/templates/course_index.html b/cms/templates/course_index.html index 92b5cc296c..e490ad7817 100644 --- a/cms/templates/course_index.html +++ b/cms/templates/course_index.html @@ -1,5 +1,6 @@ <%inherit file="base.html" /> <%block name="title">Course Manager +<%include file="widgets/header.html"/> <%block name="content">
diff --git a/cms/templates/index.html b/cms/templates/index.html index 2998cb8bd6..6e3cb648ae 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -1,8 +1,16 @@ <%inherit file="base.html" /> +<%block name="bodyclass">index <%block name="title">Courses <%block name="content"> +

edX Course Management

+
+
+

Courses

+ + +
+
    %for course, url in courses:
  1. ${course}
  2. diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index c1c05671fa..c0b9f9e3af 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,13 +1,14 @@ <%! from django.core.urlresolvers import reverse %>