From 1d768cde7a08292064428e20804898fed06a737c Mon Sep 17 00:00:00 2001
From: Simon Chen <schen@edx.org>
Date: Tue, 19 Jul 2016 19:09:51 -0400
Subject: [PATCH] ECOM-4904 Move the program editor backbone app to Studio
 (#12962)

---
 cms/djangoapps/contentstore/views/program.py  |   3 +-
 cms/static/cms/js/require-config.js           |   1 +
 cms/static/cms/js/spec/main.js                |   6 +-
 cms/static/cms/js/spec/main_squire.js         |   1 +
 cms/static/js/base.js                         |  43 +-
 .../collections/auto_auth_collection.js       |  10 +
 .../collections/course_runs_collection.js     |  59 ++
 .../collections/programs_collection.js        |  13 +
 .../js/programs/models/api_config_model.js    |  17 +
 .../js/programs/models/auto_auth_model.js     |  10 +
 cms/static/js/programs/models/course_model.js |  38 ++
 .../js/programs/models/course_run_model.js    |  17 +
 .../js/programs/models/organizations_model.js |  16 +
 .../js/programs/models/program_model.js       | 106 ++++
 cms/static/js/programs/program_admin_app.js   |  10 +
 cms/static/js/programs/router.js              |  65 +++
 cms/static/js/programs/shims/gettext.js       |  22 +
 cms/static/js/programs/utils/api_config.js    |  21 +
 cms/static/js/programs/utils/auth_utils.js    |  89 +++
 cms/static/js/programs/utils/constants.js     |  16 +
 .../js/programs/utils/validation_config.js    |  70 +++
 .../js/programs/views/confirm_modal_view.js   |  59 ++
 .../js/programs/views/course_details_view.js  | 204 +++++++
 .../js/programs/views/course_run_view.js      | 113 ++++
 .../programs/views/program_admin_app_view.js  |  68 +++
 .../js/programs/views/program_creator_view.js | 112 ++++
 .../js/programs/views/program_details_view.js | 207 +++++++
 .../js/spec/models/auto_auth_model_spec.js    | 116 ++++
 .../views/programs/program_creator_spec.js    | 217 +++++++
 .../views/programs/program_details_spec.js    | 533 ++++++++++++++++++
 cms/static/sass/_base-v2.scss                 |  15 +
 cms/static/sass/_base.scss                    |  29 -
 cms/static/sass/_build-v1.scss                |   1 +
 cms/static/sass/_build-v2.scss                |  10 +
 cms/static/sass/_mixins-v2.scss               |   4 +
 cms/static/sass/elements-v2/_controls.scss    |  45 ++
 cms/static/sass/elements-v2/_header.scss      | 172 ++++++
 cms/static/sass/elements-v2/_navigation.scss  |  22 +
 cms/static/sass/elements-v2/_sock.scss        | 135 +++++
 cms/static/sass/elements-v2/_tooltip.scss     |  25 +
 cms/static/sass/elements/_tooltip.scss        |  27 +
 cms/static/sass/partials/_variables.scss      |   1 +
 cms/static/sass/programs/_app-container.scss  |  10 +
 cms/static/sass/programs/_build.scss          |   9 +
 cms/static/sass/programs/_components.scss     |  99 ++++
 cms/static/sass/programs/_modals.scss         |  76 +++
 cms/static/sass/programs/_views.scss          |  62 ++
 cms/static/sass/studio-main-v2.scss           |   1 +
 .../js/programs/confirm_modal.underscore      |  24 +
 .../js/programs/course_details.underscore     |  49 ++
 .../js/programs/course_run.underscore         |  36 ++
 .../programs/program_creator_form.underscore  |  65 +++
 .../js/programs/program_details.underscore    |  63 +++
 cms/templates/program_authoring.html          |   4 +-
 cms/templates/widgets/header.html             |  31 +-
 cms/templates/widgets/sock.html               |   5 +-
 cms/templates/widgets/user_dropdown.html      |  54 ++
 common/test/acceptance/fixtures/programs.py   |   5 +-
 openedx/core/djangoapps/programs/models.py    |  25 +-
 .../core/djangoapps/programs/tests/mixins.py  |   2 -
 .../djangoapps/programs/tests/test_models.py  |  14 -
 openedx/core/djangoapps/programs/utils.py     |   1 -
 package.json                                  |   3 +-
 pavelib/assets.py                             |   1 +
 .../edx.org/cms/templates/widgets/sock.html   |   4 +-
 65 files changed, 3283 insertions(+), 108 deletions(-)
 create mode 100644 cms/static/js/programs/collections/auto_auth_collection.js
 create mode 100644 cms/static/js/programs/collections/course_runs_collection.js
 create mode 100644 cms/static/js/programs/collections/programs_collection.js
 create mode 100644 cms/static/js/programs/models/api_config_model.js
 create mode 100644 cms/static/js/programs/models/auto_auth_model.js
 create mode 100644 cms/static/js/programs/models/course_model.js
 create mode 100644 cms/static/js/programs/models/course_run_model.js
 create mode 100644 cms/static/js/programs/models/organizations_model.js
 create mode 100644 cms/static/js/programs/models/program_model.js
 create mode 100644 cms/static/js/programs/program_admin_app.js
 create mode 100644 cms/static/js/programs/router.js
 create mode 100644 cms/static/js/programs/shims/gettext.js
 create mode 100644 cms/static/js/programs/utils/api_config.js
 create mode 100644 cms/static/js/programs/utils/auth_utils.js
 create mode 100644 cms/static/js/programs/utils/constants.js
 create mode 100644 cms/static/js/programs/utils/validation_config.js
 create mode 100644 cms/static/js/programs/views/confirm_modal_view.js
 create mode 100644 cms/static/js/programs/views/course_details_view.js
 create mode 100644 cms/static/js/programs/views/course_run_view.js
 create mode 100644 cms/static/js/programs/views/program_admin_app_view.js
 create mode 100644 cms/static/js/programs/views/program_creator_view.js
 create mode 100644 cms/static/js/programs/views/program_details_view.js
 create mode 100644 cms/static/js/spec/models/auto_auth_model_spec.js
 create mode 100644 cms/static/js/spec/views/programs/program_creator_spec.js
 create mode 100644 cms/static/js/spec/views/programs/program_details_spec.js
 create mode 100644 cms/static/sass/_base-v2.scss
 create mode 100644 cms/static/sass/_mixins-v2.scss
 create mode 100644 cms/static/sass/elements-v2/_controls.scss
 create mode 100644 cms/static/sass/elements-v2/_header.scss
 create mode 100644 cms/static/sass/elements-v2/_navigation.scss
 create mode 100644 cms/static/sass/elements-v2/_sock.scss
 create mode 100644 cms/static/sass/elements-v2/_tooltip.scss
 create mode 100644 cms/static/sass/elements/_tooltip.scss
 create mode 100644 cms/static/sass/programs/_app-container.scss
 create mode 100644 cms/static/sass/programs/_build.scss
 create mode 100644 cms/static/sass/programs/_components.scss
 create mode 100644 cms/static/sass/programs/_modals.scss
 create mode 100644 cms/static/sass/programs/_views.scss
 create mode 100644 cms/templates/js/programs/confirm_modal.underscore
 create mode 100644 cms/templates/js/programs/course_details.underscore
 create mode 100644 cms/templates/js/programs/course_run.underscore
 create mode 100644 cms/templates/js/programs/program_creator_form.underscore
 create mode 100644 cms/templates/js/programs/program_details.underscore
 create mode 100644 cms/templates/widgets/user_dropdown.html

diff --git a/cms/djangoapps/contentstore/views/program.py b/cms/djangoapps/contentstore/views/program.py
index eceeb18f945..d95f10b9c5b 100644
--- a/cms/djangoapps/contentstore/views/program.py
+++ b/cms/djangoapps/contentstore/views/program.py
@@ -28,12 +28,11 @@ class ProgramAuthoringView(View):
 
         if programs_config.is_studio_tab_enabled and request.user.is_staff:
             return render_to_response('program_authoring.html', {
-                'show_programs_header': programs_config.is_studio_tab_enabled,
-                'authoring_app_config': programs_config.authoring_app_config,
                 'lms_base_url': '//{}/'.format(settings.LMS_BASE),
                 'programs_api_url': programs_config.public_api_url,
                 'programs_token_url': reverse('programs_id_token'),
                 'studio_home_url': reverse('home'),
+                'uses_pattern_library': True
             })
         else:
             raise Http404
diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js
index 3c84a8d3df0..dae81e88f07 100644
--- a/cms/static/cms/js/require-config.js
+++ b/cms/static/cms/js/require-config.js
@@ -56,6 +56,7 @@
             'underscore.string': 'common/js/vendor/underscore.string',
             'backbone': 'common/js/vendor/backbone',
             'backbone-relational': 'js/vendor/backbone-relational.min',
+            'backbone.validation': 'common/js/vendor/backbone-validation-min',
             'backbone.associations': 'js/vendor/backbone-associations-min',
             'backbone.paginator': 'common/js/vendor/backbone.paginator',
             'tinymce': 'js/vendor/tinymce/js/tinymce/tinymce.full.min',
diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js
index bed7aa8ee0a..5b8057f253c 100644
--- a/cms/static/cms/js/spec/main.js
+++ b/cms/static/cms/js/spec/main.js
@@ -38,6 +38,7 @@
             'backbone': 'common/js/vendor/backbone',
             'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
             'backbone.paginator': 'common/js/vendor/backbone.paginator',
+            'backbone.validation': 'common/js/vendor/backbone-validation-min',
             'backbone-relational': 'xmodule_js/common_static/js/vendor/backbone-relational.min',
             'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min',
             'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce',
@@ -267,7 +268,10 @@
         'js/certificates/spec/views/certificate_details_spec',
         'js/certificates/spec/views/certificate_editor_spec',
         'js/certificates/spec/views/certificates_list_spec',
-        'js/certificates/spec/views/certificate_preview_spec'
+        'js/certificates/spec/views/certificate_preview_spec',
+        'js/spec/models/auto_auth_model_spec',
+        'js/spec/views/programs/program_creator_spec',
+        'js/spec/views/programs/program_details_spec'
     ];
 
     i = 0;
diff --git a/cms/static/cms/js/spec/main_squire.js b/cms/static/cms/js/spec/main_squire.js
index ae59108d33c..89e19ab9833 100644
--- a/cms/static/cms/js/spec/main_squire.js
+++ b/cms/static/cms/js/spec/main_squire.js
@@ -34,6 +34,7 @@
             'backbone': 'common/js/vendor/backbone',
             'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
             'backbone.paginator': 'common/js/vendor/backbone.paginator',
+            'backbone.validation': 'common/js/vendor/backbone-validation',
             'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min',
             'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce',
             'xmodule': 'xmodule_js/src/xmodule',
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index 4ab1c9a5057..94c6cc26487 100644
--- a/cms/static/js/base.js
+++ b/cms/static/js/base.js
@@ -1,13 +1,38 @@
-require(["domReady", "jquery", "underscore", "gettext", "common/js/components/views/feedback_notification",
-        "common/js/components/views/feedback_prompt", "js/utils/date_utils",
-        "js/utils/module", "js/utils/handle_iframe_binding", "jquery.ui", "jquery.leanModal",
-        "jquery.form", "jquery.smoothScroll"],
-    function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils, IframeUtils)
+require([
+    "domReady",
+    "jquery",
+    "underscore",
+    "gettext",
+    "common/js/components/views/feedback_notification",
+    "common/js/components/views/feedback_prompt",
+    "js/utils/date_utils",
+    "js/utils/module",
+    "js/utils/handle_iframe_binding",
+    "edx-ui-toolkit/js/dropdown-menu/dropdown-menu-view", 
+    "jquery.ui",
+    "jquery.leanModal",
+    "jquery.form",
+    "jquery.smoothScroll"
+    ],
+    function(
+        domReady,
+        $,
+        _,
+        gettext,
+        NotificationView,
+        PromptView,
+        DateUtils,
+        ModuleUtils,
+        IframeUtils,
+        DropdownMenuView
+    )
 {
 
 var $body;
 
 domReady(function() {
+    var dropdownMenuView;
+    
     $body = $('body');
 
     $body.on('click', '.embeddable-xml-input', function() {
@@ -67,6 +92,14 @@ domReady(function() {
     if ($.browser.msie) {
         $.ajaxSetup({ cache: false });
     }
+
+    //Initiate the edx tool kit dropdown menu
+    if ($('.js-header-user-menu').length){
+        dropdownMenuView = new DropdownMenuView({
+            el: '.js-header-user-menu'
+        });
+        dropdownMenuView.postRender();
+    }
 });
 
 function smoothScrollLink(e) {
diff --git a/cms/static/js/programs/collections/auto_auth_collection.js b/cms/static/js/programs/collections/auto_auth_collection.js
new file mode 100644
index 00000000000..2d768df1150
--- /dev/null
+++ b/cms/static/js/programs/collections/auto_auth_collection.js
@@ -0,0 +1,10 @@
+define([
+        'backbone',
+        'js/programs/utils/auth_utils'
+    ],
+    function( Backbone, auth ) {
+        'use strict';
+
+        return Backbone.Collection.extend(auth.autoSync);
+    }
+);
diff --git a/cms/static/js/programs/collections/course_runs_collection.js b/cms/static/js/programs/collections/course_runs_collection.js
new file mode 100644
index 00000000000..71567b1bf1c
--- /dev/null
+++ b/cms/static/js/programs/collections/course_runs_collection.js
@@ -0,0 +1,59 @@
+define([
+        'backbone',
+        'jquery',
+        'js/programs/utils/api_config',
+        'js/programs/collections/auto_auth_collection',
+        'jquery.cookie'
+    ],
+    function( Backbone, $, apiConfig, AutoAuthCollection ) {
+        'use strict';
+
+        return AutoAuthCollection.extend({
+            allRuns: [],
+
+            initialize: function(models, options) {
+                // Ignore pagination and give me everything
+                var orgStr = options.organization.key,
+                    queries = '?org=' + orgStr + '&username=' + apiConfig.get('username') + '&page_size=1000';
+
+                this.url = apiConfig.get('lmsBaseUrl') + 'api/courses/v1/courses/' + queries;
+            },
+
+            /*
+             *  Abridged version of Backbone.Collection.Create that does not
+             *  save the updated Collection back to the server
+             *  (code based on original function - http://backbonejs.org/docs/backbone.html#section-134)
+             */
+            create: function(model, options) {
+                options = options ? _.clone(options) : {};
+                model = this._prepareModel(model, options);
+
+                if (!!model) {
+                    this.add(model, options);
+                    return model;
+                }
+            },
+
+            parse: function(data) {
+                this.allRuns = data.results;
+
+                // Because pagination is ignored just set results
+                return data.results;
+            },
+
+            // Adds a run back into the model for selection
+            addRun: function(id) {
+                var courseRun = _.findWhere( this.allRuns, { id: id });
+
+                this.create(courseRun);
+            },
+
+            // Removes a run from the model for selection
+            removeRun: function(id) {
+                var courseRun = this.where({id: id});
+
+                this.remove(courseRun);
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/collections/programs_collection.js b/cms/static/js/programs/collections/programs_collection.js
new file mode 100644
index 00000000000..da23f30c96b
--- /dev/null
+++ b/cms/static/js/programs/collections/programs_collection.js
@@ -0,0 +1,13 @@
+define([
+        'backbone',
+        'jquery',
+        'js/programs/models/program_model'
+    ],
+    function( Backbone, $, ProgramModel ) {
+        'use strict';
+
+        return Backbone.Collection.extend({
+            model: ProgramModel
+        });
+    }
+);
diff --git a/cms/static/js/programs/models/api_config_model.js b/cms/static/js/programs/models/api_config_model.js
new file mode 100644
index 00000000000..55fcd703169
--- /dev/null
+++ b/cms/static/js/programs/models/api_config_model.js
@@ -0,0 +1,17 @@
+define([
+        'backbone'
+    ],
+    function( Backbone ) {
+        'use strict';
+
+        return Backbone.Model.extend({
+            defaults: {
+                username: '',
+                lmsBaseUrl: '',
+                programsApiUrl: '',
+                authUrl: '/programs/id_token/',
+                idToken: ''
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/models/auto_auth_model.js b/cms/static/js/programs/models/auto_auth_model.js
new file mode 100644
index 00000000000..8a61e3a8ade
--- /dev/null
+++ b/cms/static/js/programs/models/auto_auth_model.js
@@ -0,0 +1,10 @@
+define([
+        'backbone',
+        'js/programs/utils/auth_utils'
+    ],
+    function( Backbone, auth ) {
+        'use strict';
+
+        return Backbone.Model.extend(auth.autoSync);
+    }
+);
diff --git a/cms/static/js/programs/models/course_model.js b/cms/static/js/programs/models/course_model.js
new file mode 100644
index 00000000000..b72a00207a5
--- /dev/null
+++ b/cms/static/js/programs/models/course_model.js
@@ -0,0 +1,38 @@
+define([
+        'backbone',
+        'jquery',
+        'js/programs/utils/api_config',
+        'js/programs/models/auto_auth_model',
+        'jquery.cookie',
+        'gettext'
+    ],
+    function( Backbone, $, apiConfig, AutoAuthModel ) {
+        'use strict';
+
+        return AutoAuthModel.extend({
+
+            validation: {
+                key: {
+                    required: true,
+                    maxLength: 64
+                },
+                display_name: {
+                    required: true,
+                    maxLength: 128
+                }
+            },
+
+            labels: {
+                key: gettext('Course Code'),
+                display_name: gettext('Course Title')
+            },
+
+            defaults: {
+                display_name: false,
+                key: false,
+                organization: [],
+                run_modes: []
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/models/course_run_model.js b/cms/static/js/programs/models/course_run_model.js
new file mode 100644
index 00000000000..9e41a7e52ef
--- /dev/null
+++ b/cms/static/js/programs/models/course_run_model.js
@@ -0,0 +1,17 @@
+define([
+        'backbone'
+    ],
+    function( Backbone ) {
+        'use strict';
+
+        return Backbone.Model.extend({
+            defaults: {
+                course_key: '',
+                mode_slug: 'verified',
+                sku: '',
+                start_date: '',
+                run_key: ''
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/models/organizations_model.js b/cms/static/js/programs/models/organizations_model.js
new file mode 100644
index 00000000000..19fd43f0821
--- /dev/null
+++ b/cms/static/js/programs/models/organizations_model.js
@@ -0,0 +1,16 @@
+define([
+        'js/programs/utils/api_config',
+        'js/programs/models/auto_auth_model'
+    ],
+    function( apiConfig, AutoAuthModel ) {
+        'use strict';
+
+        return AutoAuthModel.extend({
+
+            url: function() {
+                return apiConfig.get('programsApiUrl') + 'organizations/?page_size=1000';
+            }
+
+        });
+    }
+);
diff --git a/cms/static/js/programs/models/program_model.js b/cms/static/js/programs/models/program_model.js
new file mode 100644
index 00000000000..bea3f722243
--- /dev/null
+++ b/cms/static/js/programs/models/program_model.js
@@ -0,0 +1,106 @@
+define([
+        'backbone',
+        'jquery',
+        'js/programs/utils/api_config',
+        'js/programs/models/auto_auth_model',
+        'jquery.cookie'
+    ],
+    function( Backbone, $, apiConfig, AutoAuthModel ) {
+        'use strict';
+
+        return AutoAuthModel.extend({
+
+            // Backbone.Validation rules.
+            // See: http://thedersen.com/projects/backbone-validation/#configure-validation-rules-on-the-model.
+            validation: {
+                name: {
+                    required: true,
+                    maxLength: 255
+                },
+                subtitle: {
+                    // The underlying Django model does not require a subtitle.
+                    maxLength: 255
+                },
+                category: {
+                    required: true,
+                    // XSeries is currently the only valid Program type.
+                    oneOf: ['xseries']
+                },
+                organizations: 'validateOrganizations',
+                marketing_slug: {
+                    maxLength: 255
+                }
+            },
+
+            initialize: function() {
+                this.url = apiConfig.get('programsApiUrl') + 'programs/' + this.id + '/';
+            },
+
+            validateOrganizations: function( orgArray ) {
+                /**
+                 * The array passed to this method contains a single object representing
+                 * the selected organization; the object contains the organization's key.
+                 * In the future, multiple organizations might be associated with a program.
+                 */
+                var i,
+                    len = orgArray ? orgArray.length : 0;
+
+                for ( i = 0; i < len; i++ ) {
+                    if ( orgArray[i].key === 'false' ) {
+                        return gettext('Please select a valid organization.');
+                    }
+                }
+            },
+
+            getConfig: function( options ) {
+                var patch = options && options.patch,
+                    params = patch ? this.get('id') + '/' : '',
+                    config = _.extend({ validate: true, parse: true }, {
+                        type: patch ? 'PATCH' : 'POST',
+                        url: apiConfig.get('programsApiUrl') + 'programs/' + params,
+                        contentType: patch ? 'application/merge-patch+json' : 'application/json',
+                        context: this,
+                        // NB: setting context fails in tests
+                        success: _.bind( this.saveSuccess, this ),
+                        error: _.bind( this.saveError, this )
+                    });
+
+                if ( patch ) {
+                    config.data = JSON.stringify( options.update ) || this.attributes;
+                }
+
+                return config;
+            },
+
+            patch: function( data ) {
+                this.save({
+                    patch: true,
+                    update: data
+                });
+            },
+
+            save: function( options ) {
+                var method,
+                    patch = options && options.patch ? true : false,
+                    config = this.getConfig( options );
+
+                /**
+                 * Simplified version of code from the default Backbone save function
+                 * http://backbonejs.org/docs/backbone.html#section-87
+                 */
+                method = this.isNew() ? 'create' : ( patch ? 'patch' : 'update' );
+
+                this.sync( method, this, config );
+            },
+
+            saveError: function( jqXHR ) {
+                this.trigger( 'error', jqXHR );
+            },
+
+            saveSuccess: function( data ) {
+                this.set({ id: data.id });
+                this.trigger( 'sync', this );
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/program_admin_app.js b/cms/static/js/programs/program_admin_app.js
new file mode 100644
index 00000000000..1e5d293bf3b
--- /dev/null
+++ b/cms/static/js/programs/program_admin_app.js
@@ -0,0 +1,10 @@
+(function() {
+    'use strict';
+    require([
+            'js/programs/views/program_admin_app_view'
+        ],
+        function( ProgramAdminAppView ) {
+            return new ProgramAdminAppView();
+        }
+    );
+})();
diff --git a/cms/static/js/programs/router.js b/cms/static/js/programs/router.js
new file mode 100644
index 00000000000..a5ebcb1d870
--- /dev/null
+++ b/cms/static/js/programs/router.js
@@ -0,0 +1,65 @@
+define([
+        'backbone',
+        'js/programs/views/program_creator_view',
+        'js/programs/views/program_details_view',
+        'js/programs/models/program_model'
+    ],
+    function( Backbone, ProgramCreatorView, ProgramDetailsView, ProgramModel ) {
+        'use strict';
+
+        return Backbone.Router.extend({
+            root: '/program/',
+
+            routes: {
+                'new': 'programCreator',
+                ':id': 'programDetails'
+            },
+
+            initialize: function( options ) {
+                this.homeUrl = options.homeUrl;
+            },
+
+            goHome: function() {
+                window.location.href = this.homeUrl;
+            },
+
+            loadProgramDetails: function() {
+                this.programDetailsView = new ProgramDetailsView({
+                    model: this.programModel
+                });
+            },
+
+            programCreator: function() {
+                if ( this.programCreatorView ) {
+                    this.programCreatorView.destroy();
+                }
+
+                this.programCreatorView = new ProgramCreatorView({
+                    router: this
+                });
+            },
+
+            programDetails: function( id ) {
+                 this.programModel = new ProgramModel({
+                    id: id
+                });
+
+                this.programModel.on( 'sync', this.loadProgramDetails, this );
+                this.programModel.fetch();
+            },
+
+            /**
+             * Starts the router.
+             */
+            start: function () {
+                if ( !Backbone.history.started ) {
+                    Backbone.history.start({
+                        pushState: true,
+                        root: this.root
+                    });
+                }
+                return this;
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/shims/gettext.js b/cms/static/js/programs/shims/gettext.js
new file mode 100644
index 00000000000..45d4fa74500
--- /dev/null
+++ b/cms/static/js/programs/shims/gettext.js
@@ -0,0 +1,22 @@
+/**
+ * the Programs application loads gettext identity library via django, thus
+ * components reference gettext globally so a shim is added here to reflect
+ * the text so tests can be run if modules reference gettext
+ */
+(function() {
+    'use strict';
+
+    if ( !window.gettext ) {
+        window.gettext = function (text) {
+            return text;
+        };
+    }
+
+    if ( !window.interpolate ) {
+        window.interpolate = function (text) {
+            return text;
+        };
+    }
+
+    return window;
+})();
diff --git a/cms/static/js/programs/utils/api_config.js b/cms/static/js/programs/utils/api_config.js
new file mode 100644
index 00000000000..5924363b27d
--- /dev/null
+++ b/cms/static/js/programs/utils/api_config.js
@@ -0,0 +1,21 @@
+define([
+        'js/programs/models/api_config_model'
+    ],
+    function( ApiConfigModel ) {
+        'use strict';
+
+        /**
+         * This js module implements the Singleton pattern for an instance
+         * of the ApiConfigModel Backbone model.  It returns the same shared
+         * instance of that model anywhere it is required.
+         */
+        var instance;
+
+        if (instance === undefined) {
+            instance = new ApiConfigModel();
+        }
+
+        return instance;
+
+    }
+);
diff --git a/cms/static/js/programs/utils/auth_utils.js b/cms/static/js/programs/utils/auth_utils.js
new file mode 100644
index 00000000000..30bd541edf0
--- /dev/null
+++ b/cms/static/js/programs/utils/auth_utils.js
@@ -0,0 +1,89 @@
+define([
+        'jquery',
+        'underscore',
+        'js/programs/utils/api_config'
+    ],
+    function( $, _, apiConfig ) {
+        'use strict';
+
+        var auth = {
+            autoSync: {
+                /**
+                 * Override Backbone.sync to seamlessly attempt (re-)authentication when necessary.
+                 *
+                 * If a 401 error response is encountered while making a request to the Programs,
+                 * API, this wrapper will attempt to request an id token from a custom endpoint
+                 * via AJAX.  Then the original request will be retried once more.
+                 *
+                 * Any other response than 401 on the original API request, or any error occurring
+                 * on the retried API request (including 401), will be handled by the base sync
+                 * implementation.
+                 *
+                 */
+                sync: function( method, model, options ) {
+
+                    var oldError = options.error;
+
+                    this._setHeaders( options );
+
+                    options.notifyOnError = false;  // suppress Studio error pop-up that will happen if we get a 401
+
+                    options.error = function(xhr, textStatus, errorThrown) {
+                        if (xhr && xhr.status === 401) {
+                            // attempt auth and retry
+                            this._updateToken(function() {
+                                // restore the original error handler
+                                options.error = oldError;
+                                options.notifyOnError = true;  // if it fails again, let Studio notify.
+                                delete options.xhr;  // remove the failed (401) xhr from the last try.
+
+                                // update authorization header
+                                this._setHeaders( options );
+
+                                Backbone.sync.call(this, method, model, options);
+                            }.bind(this));
+                        } else if (oldError) {
+                            // fall back to the original error handler
+                            oldError.call(this, xhr, textStatus, errorThrown);
+                        }
+                    }.bind(this);
+                    return Backbone.sync.call(this, method, model, options);
+                },
+
+                /**
+                 * Fix up headers on an imminent AJAX sync, ensuring that the JWT token is enclosed
+                 * and that credentials are included when the request is being made cross-domain.
+                 */
+                _setHeaders: function( ajaxOptions ) {
+                    ajaxOptions.headers = _.extend ( ajaxOptions.headers || {}, {
+                        Authorization: 'JWT ' + apiConfig.get( 'idToken' )
+                    });
+                    ajaxOptions.xhrFields = _.extend( ajaxOptions.xhrFields || {}, {
+                        withCredentials: true
+                    });
+                },
+
+                /**
+                 * Fetch a new id token from the configured endpoint, update the api config,
+                 * and invoke the specified callback.
+                 */
+                _updateToken: function( success ) {
+
+                    $.ajax({
+                        url: apiConfig.get('authUrl'),
+                        xhrFields: {
+                            // See: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
+                            withCredentials: true
+                        },
+                        crossDomain: true
+                    }).done(function ( data ) {
+                        // save the newly-retrieved id token
+                        apiConfig.set( 'idToken', data.id_token );
+                    }).done( success );
+                }
+            }
+        };
+
+        return auth;
+    }
+);
diff --git a/cms/static/js/programs/utils/constants.js b/cms/static/js/programs/utils/constants.js
new file mode 100644
index 00000000000..e60dea9a0b1
--- /dev/null
+++ b/cms/static/js/programs/utils/constants.js
@@ -0,0 +1,16 @@
+/**
+ * Reusable constants
+ */
+define([], function() {
+    'use strict';
+
+    return {
+        keyCodes: {
+            tab: 9,
+            enter: 13,
+            esc: 27,
+            up: 38,
+            down: 40
+        }
+    };
+});
diff --git a/cms/static/js/programs/utils/validation_config.js b/cms/static/js/programs/utils/validation_config.js
new file mode 100644
index 00000000000..a0b18bd1139
--- /dev/null
+++ b/cms/static/js/programs/utils/validation_config.js
@@ -0,0 +1,70 @@
+define([
+        'backbone',
+        'backbone.validation',
+        'underscore',
+        'gettext'
+    ],
+    function( Backbone, BackboneValidation, _ ) {
+        'use strict';
+
+        var errorClass = 'has-error',
+            messageEl = '.field-message',
+            messageContent = '.field-message-content';
+
+        // These are the same messages provided by Backbone.Validation,
+        // marked for translation.
+        // See: http://thedersen.com/projects/backbone-validation/#overriding-the-default-error-messages.
+        _.extend( Backbone.Validation.messages, {
+            required: gettext( '{0} is required' ),
+            acceptance: gettext( '{0} must be accepted' ),
+            min: gettext( '{0} must be greater than or equal to {1}' ),
+            max: gettext( '{0} must be less than or equal to {1}' ),
+            range: gettext( '{0} must be between {1} and {2}' ),
+            length: gettext( '{0} must be {1} characters' ),
+            minLength: gettext( '{0} must be at least {1} characters' ),
+            maxLength: gettext( '{0} must be at most {1} characters' ),
+            rangeLength: gettext( '{0} must be between {1} and {2} characters' ),
+            oneOf: gettext( '{0} must be one of: gettext( {1}' ),
+            equalTo: gettext( '{0} must be the same as {1}' ),
+            digits: gettext( '{0} must only contain digits' ),
+            number: gettext( '{0} must be a number' ),
+            email: gettext( '{0} must be a valid email' ),
+            url: gettext( '{0} must be a valid url' ),
+            inlinePattern: gettext( '{0} is invalid' )
+        });
+
+        _.extend( Backbone.Validation.callbacks, {
+            // Gets called when a previously invalid field in the
+            // view becomes valid. Removes any error message.
+            valid: function( view, attr, selector ) {
+                var $input = view.$( '[' + selector + '~="' + attr + '"]' ),
+                    $message = $input.siblings( messageEl );
+
+                $input.removeClass( errorClass )
+                      .removeAttr( 'data-error' );
+
+                $message.removeClass( errorClass )
+                        .find( messageContent )
+                        .text( '' );
+            },
+
+            // Gets called when a field in the view becomes invalid.
+            // Adds a error message.
+            invalid: function( view, attr, error, selector ) {
+                var $input = view.$( '[' + selector + '~="' + attr + '"]' ),
+                    $message = $input.siblings( messageEl );
+
+                $input.addClass( errorClass )
+                      .attr( 'data-error', error );
+
+                $message.addClass( errorClass )
+                        .find( messageContent )
+                        .text( $input.data('error') );
+            }
+        });
+
+        Backbone.Validation.configure({
+            labelFormatter: 'label'
+        });
+    }
+);
diff --git a/cms/static/js/programs/views/confirm_modal_view.js b/cms/static/js/programs/views/confirm_modal_view.js
new file mode 100644
index 00000000000..82db156b06c
--- /dev/null
+++ b/cms/static/js/programs/views/confirm_modal_view.js
@@ -0,0 +1,59 @@
+define([
+        'backbone',
+        'jquery',
+        'underscore',
+        'js/programs/utils/constants',
+        'text!templates/programs/confirm_modal.underscore',
+        'edx-ui-toolkit/js/utils/html-utils',
+        'gettext'
+    ],
+    function( Backbone, $, _, constants, ModalTpl, HtmlUtils ) {
+        'use strict';
+
+        return Backbone.View.extend({
+            events: {
+                'click .js-cancel': 'destroy',
+                'click .js-confirm': 'confirm',
+                'keydown': 'handleKeydown'
+            },
+
+            tpl: HtmlUtils.template( ModalTpl ),
+
+            initialize: function( options ) {
+                this.$parentEl = $( options.parentEl );
+                this.callback = options.callback;
+                this.content = options.content;
+                this.render();
+            },
+
+            render: function() {
+                HtmlUtils.setHtml(this.$el, this.tpl( this.content ));
+                HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el));
+                this.postRender();
+            },
+
+            postRender: function() {
+                this.$el.find('.js-focus-first').focus();
+            },
+
+            confirm: function() {
+                this.callback();
+                this.destroy();
+            },
+
+            destroy: function() {
+                this.undelegateEvents();
+                this.remove();
+                this.$parentEl.html('');
+            },
+
+            handleKeydown: function( event ) {
+                var keyCode = event.keyCode;
+
+                if ( keyCode === constants.keyCodes.esc ) {
+                    this.destroy();
+                }
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/views/course_details_view.js b/cms/static/js/programs/views/course_details_view.js
new file mode 100644
index 00000000000..7025ec07ee0
--- /dev/null
+++ b/cms/static/js/programs/views/course_details_view.js
@@ -0,0 +1,204 @@
+define([
+        'backbone',
+        'backbone.validation',
+        'jquery',
+        'underscore',
+        'js/programs/models/course_model',
+        'js/programs/models/course_run_model',
+        'js/programs/models/program_model',
+        'js/programs/views/course_run_view',
+        'text!templates/programs/course_details.underscore',
+        'edx-ui-toolkit/js/utils/html-utils',
+        'gettext',
+        'js/programs/utils/validation_config'
+    ],
+    function( Backbone, BackboneValidation, $, _, CourseModel, CourseRunModel,
+        ProgramModel, CourseRunView, ListTpl, HtmlUtils ) {
+        'use strict';
+
+        return Backbone.View.extend({
+            parentEl: '.js-course-list',
+
+            className: 'course-details',
+
+            events: {
+                'click .js-remove-course': 'destroy',
+                'click .js-select-course': 'setCourse',
+                'click .js-add-course-run': 'addCourseRun'
+            },
+
+            tpl: HtmlUtils.template( ListTpl ),
+
+            initialize: function( options ) {
+                this.model = new CourseModel();
+                Backbone.Validation.bind( this );
+                this.$parentEl = $( this.parentEl );
+
+                // For managing subViews
+                this.courseRunViews = [];
+                this.courseRuns = options.courseRuns;
+                this.programModel = options.programModel;
+                
+                if ( options.courseData ) {
+                    this.model.set(options.courseData);
+                } else {
+                    this.model.set({run_modes: []});
+                }
+
+                // Need a unique value for field ids so using model cid
+                this.model.set({cid: this.model.cid});
+                this.model.on('change:run_modes', this.updateRuns, this);
+                this.render();
+            },
+
+            render: function() {
+                HtmlUtils.setHtml(this.$el, this.tpl(this.formatData()));
+                this.$parentEl.append( this.$el );
+                this.postRender();
+            },
+
+            postRender: function() {
+                var runs = this.model.get('run_modes');
+                if ( runs && runs.length > 0 ) {
+                    this.addCourseRuns();
+                }
+            },
+
+            addCourseRun: function(event) {
+                var $runsContainer = this.$el.find('.js-course-runs'),
+                    runModel = new CourseRunModel(),
+                    runView;
+
+                event.preventDefault();
+
+                runModel.set({course_key: undefined});
+
+                runView = new CourseRunView({
+                    model: runModel,
+                    courseModel: this.model,
+                    courseRuns: this.courseRuns,
+                    programStatus: this.programModel.get('status'),
+                    $parentEl: $runsContainer
+                });
+
+                this.courseRunViews.push( runView );
+            },
+
+            addCourseRuns: function() {
+                // Create run views
+                var runs = this.model.get('run_modes'),
+                    $runsContainer = this.$el.find('.js-course-runs');
+
+                _.each( runs, function( run ) {
+                    var runModel = new CourseRunModel(),
+                        runView;
+
+                    runModel.set(run);
+
+                    runView = new CourseRunView({
+                        model: runModel,
+                        courseModel: this.model,
+                        courseRuns: this.courseRuns,
+                        programStatus: this.programModel.get('status'),
+                        $parentEl: $runsContainer
+                    });
+
+                    this.courseRunViews.push( runView );
+
+                    return runView;
+                }.bind(this) );
+            },
+
+            addCourseToProgram: function() {
+                var courseCodes = this.programModel.get('course_codes'),
+                    courseData = this.model.toJSON();
+
+                if ( this.programModel.isValid( true ) ) {
+                    // We don't want to save the cid so omit it
+                    courseCodes.push( _.omit(courseData, 'cid') );
+                    this.programModel.patch({ course_codes: courseCodes });
+                }
+            },
+            // Delete this view
+            destroy: function() {
+                Backbone.Validation.unbind(this);
+                this.destroyChildren();
+                this.undelegateEvents();
+                this.removeCourseFromProgram();
+                this.remove();
+            },
+
+            destroyChildren: function() {
+                var runs = this.courseRunViews;
+
+                _.each( runs, function( run ) {
+                    run.removeRun();
+                });
+            },
+
+            // Format data to be passed to the template
+            formatData: function() {
+                var data = $.extend( {}, 
+                    { courseRuns: this.courseRuns.models },
+                    _.omit( this.programModel.toJSON(), 'run_modes'),
+                    this.model.toJSON()
+                );
+
+                return data;
+            },
+
+            removeCourseFromProgram: function() {
+                var courseCodes = this.programModel.get('course_codes'),
+                    key = this.model.get('key'),
+                    name = this.model.get('display_name'),
+                    update = [];
+
+                update = _.reject( courseCodes, function(course) {
+                    return course.key === key && course.display_name === name;
+                });
+
+                this.programModel.patch({ course_codes: update });
+            },
+
+            setCourse: function( event ) {
+                var $form = this.$('.js-course-form'),
+                    title = $form.find('.display-name').val(),
+                    key = $form.find('.course-key').val();
+
+                event.preventDefault();
+
+                this.model.set({
+                    display_name: title,
+                    key: key,
+                    organization: this.programModel.get('organizations')[0]
+                });
+
+                if ( this.model.isValid(true) ) {
+                    this.addCourseToProgram();
+                    this.updateDOM();
+                    this.addCourseRuns();
+                }
+            },
+
+            updateDOM: function() {
+                HtmlUtils.setHtml(this.$el, this.tpl( this.formatData() ) );
+            },
+
+            updateRuns: function() {
+                var courseCodes = this.programModel.get('course_codes'),
+                    key = this.model.get('key'),
+                    name = this.model.get('display_name'),
+                    index;
+
+                if ( this.programModel.isValid( true ) ) {
+                    index = _.findIndex( courseCodes, function(course) {
+                        return course.key === key && course.display_name === name;
+                    });
+                    courseCodes[index] = this.model.toJSON();
+
+                    this.programModel.patch({ course_codes: courseCodes });
+                }
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/views/course_run_view.js b/cms/static/js/programs/views/course_run_view.js
new file mode 100644
index 00000000000..7284493b668
--- /dev/null
+++ b/cms/static/js/programs/views/course_run_view.js
@@ -0,0 +1,113 @@
+define([
+        'backbone',
+        'jquery',
+        'underscore',
+        'text!templates/programs/course_run.underscore',
+        'edx-ui-toolkit/js/utils/html-utils'
+    ],
+    function ( Backbone, $, _, CourseRunTpl, HtmlUtils ) {
+        'use strict';
+
+        return Backbone.View.extend({
+            events: {
+                'change .js-course-run-select': 'selectRun',
+                'click .js-remove-run': 'removeRun'
+            },
+
+            tpl: HtmlUtils.template( CourseRunTpl ),
+
+            initialize: function( options ) {
+                /**
+                 * Need the run model for the template, and the courseModel
+                 * to keep parent view up to date with run changes
+                 */
+                this.courseModel = options.courseModel;
+                this.courseRuns = options.courseRuns;
+                this.programStatus = options.programStatus;
+
+                this.model.on('change', this.render, this);
+                this.courseRuns.on('update', this.updateDropdown, this);
+
+                this.$parentEl = options.$parentEl;
+                this.render();
+            },
+
+            render: function() {
+                var data = this.model.attributes;
+
+                data.programStatus = this.programStatus;
+
+                if ( !!this.courseRuns ) {
+                    data.courseRuns = this.courseRuns.toJSON();
+                }
+
+                HtmlUtils.setHtml(this.$el, this.tpl( data ) );
+                this.$parentEl.append( this.$el );
+            },
+
+            // Delete this view
+            destroy: function() {
+                this.undelegateEvents();
+                this.remove();
+            },
+
+            // Data returned from courseList API is not the correct format
+            formatData: function( data ) {
+                return {
+                    course_key: data.id,
+                    mode_slug: 'verified',
+                    start_date: data.start,
+                    sku: ''
+                };
+            },
+
+            removeRun: function() {
+                // Update run_modes array on programModel
+                var startDate = this.model.get('start_date'),
+                    courseKey = this.model.get('course_key'),
+                    /**
+                     *  NB: cloning the array so the model will fire a change event when
+                     *  the updated version is saved back to the model
+                     */
+                    runs = _.clone(this.courseModel.get('run_modes')),
+                    updatedRuns = [];
+
+                updatedRuns = _.reject( runs, function( obj ) {
+                    return obj.start_date === startDate &&
+                           obj.course_key === courseKey;
+                });
+
+                this.courseModel.set({
+                    run_modes: updatedRuns
+                });
+
+                this.courseRuns.addRun(courseKey);
+
+                this.destroy();
+            },
+
+            selectRun: function(event) {
+                var id = $(event.currentTarget).val(),
+                    runObj = _.findWhere(this.courseRuns.allRuns, {id: id}),
+                    /**
+                     *  NB: cloning the array so the model will fire a change event when
+                     *  the updated version is saved back to the model
+                     */
+                    runs = _.clone(this.courseModel.get('run_modes')),
+                    data = this.formatData(runObj);
+
+                this.model.set( data );
+                runs.push(data);
+                this.courseModel.set({run_modes: runs});
+                this.courseRuns.removeRun(id);
+            },
+
+            // If a run has not been selected update the dropdown options
+            updateDropdown: function() {
+                if ( !this.model.get('course_key') ) {
+                    this.render();
+                }
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/views/program_admin_app_view.js b/cms/static/js/programs/views/program_admin_app_view.js
new file mode 100644
index 00000000000..4f0a5e184b0
--- /dev/null
+++ b/cms/static/js/programs/views/program_admin_app_view.js
@@ -0,0 +1,68 @@
+(function() {
+    'use strict';
+
+    define([
+            'backbone',
+            'js/programs/router',
+            'js/programs/utils/api_config'
+        ],
+        function( Backbone, ProgramRouter, apiConfig ) {
+            return Backbone.View.extend({
+                el: '.js-program-admin',
+
+                events: {
+                    'click .js-app-click': 'navigate'
+                },
+
+                initialize: function() {
+                    apiConfig.set({
+                        lmsBaseUrl: this.$el.data('lms-base-url'),
+                        programsApiUrl: this.$el.data('programs-api-url'),
+                        authUrl: this.$el.data('auth-url'),
+                        username: this.$el.data('username')
+                    });
+
+                    this.app = new ProgramRouter({
+                        homeUrl: this.$el.data('home-url')
+                    });
+                    this.app.start();
+                },
+
+                /**
+                 * Navigate to a new page within the app.
+                 *
+                 * Attempts to open the link in a new tab/window behave as the user expects, however the app
+                 * and data will be reloaded in the new tab/window.
+                 *
+                 * @param {Event} event - Event being handled.
+                 * @returns {boolean} - Indicates if event handling succeeded (always true).
+                 */
+                navigate: function (event) {
+                    var url = $(event.target).attr('href').replace( this.app.root, '' );
+
+                    /**
+                     * Handle the cases where the user wants to open the link in a new tab/window.
+                     * event.which === 2 checks for the middle mouse button (https://api.jquery.com/event.which/)
+                     */
+                    if ( event.ctrlKey || event.shiftKey || event.metaKey || event.which === 2 ) {
+                        return true;
+                    }
+
+                    // We'll take it from here...
+                    event.preventDefault();
+
+                    // Process the navigation in the app/router.
+                    if ( url === Backbone.history.getFragment() && url === '' ) {
+                        /**
+                         * Note: We must call the index directly since Backbone
+                         * does not support routing to the same route.
+                         */
+                        this.app.index();
+                    } else {
+                        this.app.navigate( url, { trigger: true } );
+                    }
+                }
+            });
+        }
+    );
+})();
diff --git a/cms/static/js/programs/views/program_creator_view.js b/cms/static/js/programs/views/program_creator_view.js
new file mode 100644
index 00000000000..6f07486cdf9
--- /dev/null
+++ b/cms/static/js/programs/views/program_creator_view.js
@@ -0,0 +1,112 @@
+define([
+        'backbone',
+        'backbone.validation',
+        'jquery',
+        'underscore',
+        'js/programs/models/organizations_model',
+        'js/programs/models/program_model',
+        'text!templates/programs/program_creator_form.underscore',
+        'edx-ui-toolkit/js/utils/html-utils',
+        'gettext',
+        'js/programs/utils/validation_config'
+    ],
+    function ( Backbone, BackboneValidation, $, _, OrganizationsModel, ProgramModel, ListTpl, HtmlUtils ) {
+        'use strict';
+
+        return Backbone.View.extend({
+            parentEl: '.js-program-admin',
+
+            events: {
+                'click .js-create-program': 'createProgram',
+                'click .js-abort-view': 'abort'
+            },
+
+            tpl: HtmlUtils.template( ListTpl ),
+
+            initialize: function( options ) {
+                this.$parentEl = $( this.parentEl );
+
+                this.model = new ProgramModel();
+                this.model.on( 'sync', this.saveSuccess, this );
+                this.model.on( 'error', this.saveError, this );
+
+                // Hook up validation.
+                // See: http://thedersen.com/projects/backbone-validation/#validation-binding.
+                Backbone.Validation.bind( this );
+
+                this.organizations = new OrganizationsModel();
+                this.organizations.on( 'sync', this.render, this );
+                this.organizations.fetch();
+
+                this.router = options.router;
+            },
+
+            render: function() {
+                HtmlUtils.setHtml(
+                    this.$el,
+                    this.tpl( {
+                        orgs: this.organizations.get('results')
+                    })
+                );
+
+                HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML( this.$el ));
+            },
+
+            abort: function( event ) {
+                event.preventDefault();
+                this.router.goHome();
+            },
+
+            createProgram: function( event ) {
+                var data = this.getData();
+
+                event.preventDefault();
+                this.model.set( data );
+
+                // Check if the model is valid before saving. Invalid attributes are looked
+                // up by name. The corresponding elements receieve an `invalid` class and a
+                // `data-error` attribute. Both are removed when formerly invalid attributes
+                // become valid.
+                // See: http://thedersen.com/projects/backbone-validation/#isvalid.
+                if ( this.model.isValid(true) ) {
+                    this.model.save();
+                }
+            },
+
+            destroy: function() {
+                // Unhook validation.
+                // See: http://thedersen.com/projects/backbone-validation/#unbinding.
+                Backbone.Validation.unbind(this);
+
+                this.undelegateEvents();
+                this.remove();
+            },
+
+            getData: function() {
+                return {
+                    name: this.$el.find( '.program-name' ).val(),
+                    subtitle: this.$el.find( '.program-subtitle' ).val(),
+                    category: this.$el.find( '.program-type' ).val(),
+                    marketing_slug: this.$el.find( '.program-marketing-slug' ).val(),
+                    organizations: [{
+                        key: this.$el.find( '.program-org' ).val()
+                    }]
+                };
+            },
+
+            goToView: function( uri ) {
+                Backbone.history.navigate( uri, { trigger: true } );
+                this.destroy();
+            },
+
+            // TODO: add user messaging to show errors
+            saveError: function( jqXHR ) {
+                console.log( 'saveError: ', jqXHR );
+            },
+
+            saveSuccess: function() {
+                this.goToView( String( this.model.get( 'id' ) ) );
+            }
+        });
+    }
+);
diff --git a/cms/static/js/programs/views/program_details_view.js b/cms/static/js/programs/views/program_details_view.js
new file mode 100644
index 00000000000..2fb9fafc6a1
--- /dev/null
+++ b/cms/static/js/programs/views/program_details_view.js
@@ -0,0 +1,207 @@
+define([
+        'backbone',
+        'backbone.validation',
+        'jquery',
+        'underscore',
+        'js/programs/collections/course_runs_collection',
+        'js/programs/models/program_model',
+        'js/programs/views/confirm_modal_view',
+        'js/programs/views/course_details_view',
+        'text!templates/programs/program_details.underscore',
+        'edx-ui-toolkit/js/utils/html-utils',
+        'gettext',
+        'js/programs/utils/validation_config'
+    ],
+    function( Backbone, BackboneValidation, $, _, CourseRunsCollection,
+        ProgramModel, ModalView, CourseView, ListTpl,
+            HtmlUtils ) {
+        'use strict';
+
+        return Backbone.View.extend({
+            el: '.js-program-admin',
+
+            events: {
+                'blur .js-inline-edit input': 'checkEdit',
+                'click .js-add-course': 'addCourse',
+                'click .js-enable-edit': 'editField',
+                'click .js-publish-program': 'confirmPublish'
+            },
+
+            tpl: HtmlUtils.template( ListTpl ),
+
+            initialize: function() {
+                Backbone.Validation.bind( this );
+
+                this.courseRuns = new CourseRunsCollection([], {
+                    organization: this.model.get('organizations')[0]
+                });
+                this.courseRuns.fetch();
+                this.courseRuns.on('sync', this.setAvailableCourseRuns, this);
+                this.render();
+            },
+
+            render: function() {
+                HtmlUtils.setHtml(this.$el, this.tpl( this.model.toJSON() ) );
+                this.postRender();
+            },
+
+            postRender: function() {
+                var courses = this.model.get( 'course_codes' );
+
+                _.each( courses, function( course ) {
+                    var title = course.key + 'Course';
+
+                    this[ title ] = new CourseView({
+                        courseRuns: this.courseRuns,
+                        programModel: this.model,
+                        courseData: course
+                    });
+                }.bind(this) );
+
+                // Stop listening to the model sync set when publishing
+                this.model.off( 'sync' );
+            },
+
+            addCourse: function() {
+                return new CourseView({
+                    courseRuns: this.courseRuns,
+                    programModel: this.model
+                });
+            },
+
+            checkEdit: function( event ) {
+                var $input = $(event.target),
+                    $span = $input.prevAll('.js-model-value'),
+                    $btn = $input.next('.js-enable-edit'),
+                    value = $input.val(),
+                    key = $input.data('field'),
+                    data = {};
+
+                data[key] = value;
+
+                $input.addClass('is-hidden');
+                $btn.removeClass('is-hidden');
+                $span.removeClass('is-hidden');
+
+                if ( this.model.get( key ) !== value ) {
+                    this.model.set( data );
+
+                    if ( this.model.isValid( true ) ) {
+                        this.model.patch( data );
+                        $span.text( value );
+                    }
+                }
+            },
+
+            /**
+             * Loads modal that user clicks a confirmation button
+             * in to publish the course (or they can cancel out of it)
+             */
+            confirmPublish: function( event ) {
+                event.preventDefault();
+
+                /**
+                 * Update validation to make marketing slug required
+                 * Note that because this validation is not required for
+                 * the program creation form and is only happening here
+                 * it makes sense to have the validation at the view level
+                 */
+                if ( this.model.isValid( true ) && this.validateMarketingSlug() ) {
+                    this.modalView = new ModalView({
+                        model: this.model,
+                        callback: _.bind( this.publishProgram, this ),
+                        content: this.getModalContent(),
+                        parentEl: '.js-publish-modal',
+                        parentView: this
+                    });
+                }
+            },
+
+            editField: function( event ) {
+                /**
+                 * Making the assumption that users can only see
+                 * programs that they have permission to edit
+                 */
+                var $btn = $( event.currentTarget ),
+                    $el = $btn.prev( 'input' );
+
+                event.preventDefault();
+
+                $el.prevAll( '.js-model-value' ).addClass( 'is-hidden' );
+                $el.removeClass( 'is-hidden' )
+                   .addClass( 'edit' )
+                   .focus();
+                $btn.addClass( 'is-hidden' );
+            },
+
+            getModalContent: function() {
+                /* jshint maxlen: 300 */
+                return {
+                    name: gettext('confirm'),
+                    title: gettext('Publish this program?'),
+                    body: gettext(
+                        'After you publish this program, you cannot add or remove course codes or remove course runs.'
+                    ),
+                    cta: {
+                        cancel: gettext('Cancel'),
+                        confirm: gettext('Publish')
+                    }
+                };
+            },
+
+            publishProgram: function() {
+                var data = {
+                    status: 'active'
+                };
+
+                this.model.set( data, { silent: true } );
+                this.model.on( 'sync', this.render, this );
+                this.model.patch( data );
+            },
+
+            setAvailableCourseRuns: function() {
+                var allRuns = this.courseRuns.toJSON(),
+                    courses = this.model.get('course_codes'),
+                    selectedRuns,
+                    availableRuns = allRuns;
+
+                if (courses.length) {
+                    selectedRuns = _.pluck( courses, 'run_modes' );
+                    selectedRuns = _.flatten( selectedRuns );
+                }
+
+                availableRuns = _.reject(allRuns, function(run) {
+                    var selectedCourseRun = _.findWhere( selectedRuns, {
+                        course_key: run.id,
+                        start_date: run.start
+                    });
+
+                    return !_.isUndefined(selectedCourseRun);
+                });
+
+                this.courseRuns.set(availableRuns);
+            },
+
+            validateMarketingSlug: function() {
+                var isValid = false,
+                    $input = {},
+                    $message = {};
+
+                if ( this.model.get( 'marketing_slug' ).length > 0 ) {
+                    isValid = true;
+                } else {
+                    $input = this.$el.find( '#program-marketing-slug' );
+                    $message = $input.siblings( '.field-message' );
+
+                    // Update DOM
+                    $input.addClass( 'has-error' );
+                    $message.addClass( 'has-error' );
+                    $message.find( '.field-message-content' )
+                        .text( gettext( 'Marketing Slug is required.') );
+                }
+
+                return isValid;
+            }
+        });
+    }
+);
diff --git a/cms/static/js/spec/models/auto_auth_model_spec.js b/cms/static/js/spec/models/auto_auth_model_spec.js
new file mode 100644
index 00000000000..bdec67c1be5
--- /dev/null
+++ b/cms/static/js/spec/models/auto_auth_model_spec.js
@@ -0,0 +1,116 @@
+define([
+        'underscore',
+        'backbone',
+        'jquery',
+        'js/programs/utils/api_config',
+        'js/programs/models/auto_auth_model'
+    ],
+    function( _, Backbone, $, apiConfig, AutoAuthModel ) {
+        'use strict';
+
+        describe('AutoAuthModel', function () {
+
+            var model,
+                testErrorCallback,
+                fakeAjaxDeferred,
+                spyOnBackboneSync,
+                callSync,
+                checkAuthAttempted,
+                dummyModel = {'dummy': 'model'},
+                authUrl = apiConfig.get( 'authUrl' );
+
+            beforeEach( function() {
+
+                // instance under test
+                model = new AutoAuthModel();
+
+                // stand-in for the error callback a caller might pass with options to Backbone.Model.sync
+                testErrorCallback = jasmine.createSpy();
+
+                fakeAjaxDeferred = $.Deferred();
+                spyOn( $, 'ajax' ).and.returnValue( fakeAjaxDeferred );
+                return fakeAjaxDeferred;
+
+            });
+
+            spyOnBackboneSync = function( status ) {
+                // set up Backbone.sync to invoke its error callback with the desired HTTP status
+                spyOn( Backbone, 'sync' ).and.callFake( function(method, model, options) {
+                    var fakeXhr = options.xhr = { status: status };
+                    options.error(fakeXhr, 0, '');
+                });
+            };
+
+            callSync = function(options) {
+                var params,
+                    syncOptions = _.extend( { error: testErrorCallback }, options || {} );
+
+                model.sync('GET', dummyModel, syncOptions);
+
+                // make sure Backbone.sync was called with custom error handling
+                expect( Backbone.sync.calls.count() ).toEqual(1);
+                params = _.object( ['method', 'model', 'options'], Backbone.sync.calls.mostRecent().args );
+                expect( params.method ).toEqual( 'GET' );
+                expect( params.model ).toEqual( dummyModel );
+                expect( params.options.error ).not.toEqual( testErrorCallback );
+                return params;
+            };
+
+            checkAuthAttempted = function(isExpected) {
+                if (isExpected) {
+                    expect( $.ajax ).toHaveBeenCalled();
+                    expect( $.ajax.calls.mostRecent().args[0].url ).toEqual( authUrl );
+                } else {
+                    expect( $.ajax ).not.toHaveBeenCalled();
+                }
+            };
+
+            it( 'should exist', function () {
+                expect( model ).toBeDefined();
+            });
+
+            it( 'should intercept 401 errors and attempt auth', function() {
+
+                var callParams;
+
+                spyOnBackboneSync(401);
+
+                callSync();
+
+                // make sure the auth attempt was initiated
+                checkAuthAttempted(true);
+
+                // fire the success handler for the fake ajax call, with id token response data
+                fakeAjaxDeferred.resolve( {id_token: 'test-id-token'} );
+
+                // make sure the original request was retried with token, and without custom error handling
+                expect( Backbone.sync.calls.count() ).toEqual(2);
+                callParams = _.object( ['method', 'model', 'options'], Backbone.sync.calls.mostRecent().args );
+                expect( callParams.method ).toEqual( 'GET' );
+                expect( callParams.model ).toEqual( dummyModel );
+                expect( callParams.options.error ).toEqual( testErrorCallback );
+                expect( callParams.options.headers.Authorization ).toEqual( 'JWT test-id-token' );
+
+            });
+
+            it( 'should not intercept non-401 errors', function() {
+
+                spyOnBackboneSync(403);
+
+                // invoke AutoAuthModel.sync
+                callSync();
+
+                // make sure NO auth attempt was initiated
+                checkAuthAttempted(false);
+
+                // make sure the original request was not retried
+                expect( Backbone.sync.calls.count() ).toEqual(1);
+
+                // make sure the default error handling was invoked
+                expect( testErrorCallback ).toHaveBeenCalled();
+
+            });
+
+        });
+    }
+);
diff --git a/cms/static/js/spec/views/programs/program_creator_spec.js b/cms/static/js/spec/views/programs/program_creator_spec.js
new file mode 100644
index 00000000000..21a59d6e6a0
--- /dev/null
+++ b/cms/static/js/spec/views/programs/program_creator_spec.js
@@ -0,0 +1,217 @@
+define([
+        'backbone',
+        'jquery',
+        'js/programs/views/program_creator_view'
+    ],
+    function( Backbone, $, ProgramCreatorView ) {
+        'use strict';
+
+        describe('ProgramCreatorView', function () {
+            var view = {},
+                Router = Backbone.Router.extend({
+                    initialize: function( options ) {
+                        this.homeUrl = options.homeUrl;
+                    },
+                    goHome: function() {
+                        window.location.href = this.homeUrl;
+                    }
+                }),
+                organizations = {
+                    count: 1,
+                    previous: null,
+                    'num_pages': 1,
+                    results:[{
+                        'display_name': 'test-org-display_name',
+                        'key': 'test-org-key'
+                    }],
+                    next: null
+                },
+                sampleInput = {
+                    organizations: 'test-org-key',
+                    name: 'Test Course Name',
+                    subtitle: 'Test Course Subtitle',
+                    marketing_slug: 'test-management'
+                },
+                completeForm = function( data ) {
+                    view.$el.find('#program-name').val( data.name );
+                    view.$el.find('#program-subtitle').val( data.subtitle );
+                    view.$el.find('#program-org').val( data.organizations );
+
+                    if ( data.category ) {
+                        view.$el.find('#program-type').val( data.category );
+                    }
+
+                    if ( data.marketing_slug ) {
+                        view.$el.find('#program-marketing-slug').val( data.marketing_slug );
+                    }
+                },
+                verifyValidation = function ( data, invalidAttr ) {
+                    var errorClass = 'has-error',
+                        $invalidElement = view.$el.find( '[name="' + invalidAttr + '"]' ),
+                        $errorMsg = $invalidElement.siblings('.field-message'),
+                        inputErrorMsg = '';
+
+                    completeForm( data );
+
+                    view.$el.find('.js-create-program').click();
+                    inputErrorMsg = $invalidElement.data('error');
+
+                    expect( view.model.save ).not.toHaveBeenCalled();
+                    expect( $invalidElement ).toHaveClass( errorClass );
+                    expect( $errorMsg ).toHaveClass( errorClass );
+                    expect( inputErrorMsg ).toBeDefined();
+                    expect( $errorMsg.find('.field-message-content').html() ).toEqual( inputErrorMsg );
+                };
+
+            beforeEach( function() {
+                // Set the DOM
+                setFixtures( '<div class="js-program-admin"></div>' );
+
+                jasmine.clock().install();
+
+                spyOn( ProgramCreatorView.prototype, 'saveSuccess' ).and.callThrough();
+                spyOn( ProgramCreatorView.prototype, 'goToView' ).and.callThrough();
+                spyOn( ProgramCreatorView.prototype, 'saveError' ).and.callThrough();
+                spyOn( Router.prototype, 'goHome' );
+
+                view = new ProgramCreatorView({
+                    router: new Router({
+                        homeUrl: '/author/home'
+                    })
+                });
+
+                view.organizations.set( organizations );
+                view.render();
+            });
+
+            afterEach( function() {
+                view.destroy();
+
+                jasmine.clock().uninstall();
+            });
+
+            it( 'should exist', function () {
+                expect( view ).toBeDefined();
+            });
+
+            it ( 'should get the form data', function() {
+                var formData = {};
+
+                completeForm( sampleInput );
+                formData = view.getData();
+
+                expect( formData.name ).toEqual( sampleInput.name );
+                expect( formData.subtitle ).toEqual( sampleInput.subtitle );
+                expect( formData.organizations[0].key ).toEqual( sampleInput.organizations );
+            });
+
+            it( 'should submit the form when the user clicks submit', function() {
+                var programId = 123;
+
+                completeForm( sampleInput );
+
+                spyOn( $, 'ajax' ).and.callFake( function( event ) {
+                    event.success({ id: programId });
+                });
+
+                view.$el.find('.js-create-program').click();
+
+                expect( $.ajax ).toHaveBeenCalled();
+                expect( view.saveSuccess ).toHaveBeenCalled();
+                expect( view.goToView ).toHaveBeenCalledWith( String( programId ) );
+                expect( view.saveError ).not.toHaveBeenCalled();
+            });
+
+            it( 'should run the saveError when model save failures occur', function() {
+                spyOn( $, 'ajax' ).and.callFake( function( event ) {
+                    event.error();
+                });
+
+                // Fill out the form with valid data so that form model validation doesn't
+                // prevent the model from being saved.
+                completeForm( sampleInput );
+                view.$el.find('.js-create-program').click();
+
+                expect( $.ajax ).toHaveBeenCalled();
+                expect( view.saveSuccess ).not.toHaveBeenCalled();
+                expect( view.saveError ).toHaveBeenCalled();
+            });
+
+            it( 'should set the model when valid form data is submitted', function() {
+                completeForm( sampleInput );
+
+                spyOn( $, 'ajax' ).and.callFake( function( event ) {
+                    event.success({ id: 10001110101 });
+                });
+
+                view.$el.find('.js-create-program').click();
+
+                expect( view.model.get('name') ).toEqual( sampleInput.name );
+                expect( view.model.get('subtitle') ).toEqual( sampleInput.subtitle );
+                expect( view.model.get('organizations')[0].key ).toEqual( sampleInput.organizations );
+                expect( view.model.get('marketing_slug') ).toEqual( sampleInput.marketing_slug );
+            });
+
+            it( 'should not set the model when an invalid program name is submitted', function() {
+                var invalidInput = $.extend({}, sampleInput);
+
+                spyOn( view.model, 'save' );
+
+                // No name provided.
+                invalidInput.name = '';
+                verifyValidation( invalidInput, 'name' );
+
+                // Name is too long.
+                invalidInput.name = 'x'.repeat(256);
+                verifyValidation( invalidInput, 'name' );
+            });
+
+            it( 'should not set the model when an invalid program subtitle is submitted', function() {
+                var invalidInput = $.extend({}, sampleInput);
+
+                spyOn( view.model, 'save' );
+
+                // Subtitle is too long.
+                invalidInput.subtitle = 'x'.repeat(300);
+                verifyValidation( invalidInput, 'subtitle' );
+            });
+
+            it( 'should not set the model when an invalid category is submitted', function() {
+                var invalidInput = $.extend({}, sampleInput);
+
+                spyOn( view.model, 'save' );
+
+                // Category other than 'xseries' selected.
+                invalidInput.category = 'yseries';
+                verifyValidation( invalidInput, 'category' );
+            });
+
+            it( 'should not set the model when an invalid organization key is submitted', function() {
+                var invalidInput = $.extend({}, sampleInput);
+
+                spyOn( view.model, 'save' );
+
+                // No organization selected.
+                invalidInput.organizations = 'false';
+                verifyValidation( invalidInput, 'organizations' );
+            });
+
+            it( 'should not set the model when an invalid marketing slug is submitted', function() {
+                var invalidInput = $.extend({}, sampleInput);
+
+                spyOn( view.model, 'save' );
+
+                // Marketing slug is too long.
+                invalidInput.marketing_slug = 'x'.repeat(256);
+                verifyValidation( invalidInput, 'marketing_slug' );
+            });
+
+            it( 'should abort the view when the cancel button is clicked', function() {
+                completeForm( sampleInput );
+                expect( view.$parentEl.html().length ).toBeGreaterThan( 0 );
+                view.$el.find('.js-abort-view').click();
+                expect( view.router.goHome ).toHaveBeenCalled();
+            });
+        });
+    }
+);
diff --git a/cms/static/js/spec/views/programs/program_details_spec.js b/cms/static/js/spec/views/programs/program_details_spec.js
new file mode 100644
index 00000000000..b84b123dd82
--- /dev/null
+++ b/cms/static/js/spec/views/programs/program_details_spec.js
@@ -0,0 +1,533 @@
+define([
+        'jquery',
+        'js/programs/collections/course_runs_collection',
+        'js/programs/models/program_model',
+        'js/programs/views/course_run_view',
+        'js/programs/views/program_details_view',
+        'js/programs/utils/constants'
+    ],
+    function( $, CourseRunsCollection, ProgramModel, CourseRunView,
+              ProgramDetailsView, constants ) {
+        'use strict';
+
+        /* jshint maxlen: 300 */
+        describe('ProgramDetailsView', function () {
+            var view = {},
+                model = {},
+                courseRunsList = [
+                    {
+                        id: 'course-v1:edX+DemoX+Demo_Course',
+                        name: 'edX Demonstration Course',
+                        number: 'DemoX',
+                        org: 'edX',
+                        short_description: null,
+                        effort: null,
+                        media: {
+                            course_image: {
+                                uri: '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg'
+                            },
+                            course_video: {
+                                uri: null
+                            }
+                        },
+                        start: 'May 23, 2015',
+                        start_type: 'timestamp',
+                        start_display: 'Feb. 5, 2013',
+                        end: null,
+                        enrollment_start: null,
+                        enrollment_end: null,
+                        blocks_url: 'http://127.0.0.1:8000/api/courses/v1/blocks/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course'
+                    },
+                    {
+                        id: 'course-v1:edx+Krampus25+2015_12_05',
+                        name: 'Krampusnacht',
+                        number: 'Krampus25',
+                        org: 'edx',
+                        short_description: null,
+                        effort: null,
+                        media: {
+                            course_image: {
+                                uri: '/asset-v1:edx+Krampus25+2015_12_05+type@asset+block@images_course_image.jpg'
+                            },
+                            course_video: {
+                                uri: null
+                            }
+                        },
+                        start: '2030-01-01T00:00:00Z',
+                        start_type: 'empty',
+                        start_display: null,
+                        end: null,
+                        enrollment_start: null,
+                        enrollment_end: null,
+                        blocks_url: 'http://127.0.0.1:8000/api/courses/v1/blocks/?course_id=course-v1%3Aedx%2BKrampus25%2B2015_12_05'
+                    },
+                    {
+                        id: 'course-v1:edx+shiaLB101+2016_01',
+                        name: 'Shia "The Beef"',
+                        number: 'shiaLB101',
+                        org: 'edx',
+                        short_description: null,
+                        effort: null,
+                        media: {
+                            course_image: {
+                                uri: '/asset-v1:edx+shiaLB101+2016_01+type@asset+block@images_course_image.jpg'
+                            },
+                            course_video: {
+                                uri: null
+                            }
+                        },
+                        start: '2030-01-01T00:00:00Z',
+                        start_type: 'empty',
+                        start_display: null,
+                        end: null,
+                        enrollment_start: null,
+                        enrollment_end: null,
+                        blocks_url: 'http://127.0.0.1:8000/api/courses/v1/blocks/?course_id=course-v1%3Aedx%2BshiaLB101%2B2016_01'
+                    }
+                ],
+                programData = {
+                    category: 'xseries',
+                    course_codes: [{
+                        display_name: 'test-course-display_name',
+                        key: 'test-course-key',
+                        organization: {
+                            display_name: 'test-org-display_name',
+                            key: 'test-org-key'
+                        },
+                        run_modes: [
+                            {
+                                course_key: 'course-v1:edX+DemoX+Demo_Course',
+                                mode_slug: 'honor',
+                                sku: null,
+                                start: 'May 23, 2015'
+                            }, {
+                                course_key: 'course-v1:edX+DemoX+Demo_Course',
+                                mode_slug: 'honor',
+                                sku: null,
+                                start: 'August 01, 2015'
+                            }, {
+                                course_key: 'course-v1:edX+DemoX+Demo_Course',
+                                mode_slug: 'honor',
+                                sku: null,
+                                start: 'December 11, 2015'
+                            }
+                        ]
+                    }],
+                    created: '2015-10-20T18:11:46.854451Z',
+                    id: 5,
+                    marketing_slug: 'test-program-slug',
+                    modified: '2015-10-20T18:11:46.854735Z',
+                    name: 'test-program-5',
+                    organizations: [{
+                        display_name: 'test-org-display_name',
+                        key: 'test-org-key'
+                    }],
+                    status: 'unpublished',
+                    subtitle: 'test-subtitle'
+                },
+                testTimeoutInterval = 100,
+                errorClass = 'has-error',
+                addCourse,
+                completeCourseForm,
+                dropdownSelect,
+                editField,
+                keyPress,
+                openPublishModal,
+                resetCourseCodes,
+                testHidingButtonsAfterPublish,
+                testInvalidUpdate,
+                testUnchangedFieldBlur,
+                testUpdatedFieldBlur;
+
+            addCourse = function() {
+                var $addCourseBtn = view.$el.find('.js-add-course').first(),
+                    $form,
+                    $submitBtn;
+
+                expect( view.$el.find('.course-details').length ).toEqual( 1 );
+                $addCourseBtn.click();
+                $form = view.$('.js-course-form');
+                $submitBtn = $form.find('.js-select-course');
+                completeCourseForm();
+                $submitBtn.click();
+                expect( $form.find('.field-message.has-error').length ).toEqual( 0 );
+            };
+
+            completeCourseForm = function() {
+                var $form = view.$('.js-course-form');
+
+                $form.find('.course-key').val('123');
+                $form.find('.display-name').val('test course 1');
+            };
+
+            dropdownSelect = function( $select, value ) {
+                $select.find('option:selected').prop('selected', false);
+                $select.val(value).prop('selected', true);
+                $select.trigger('change');
+            };
+
+            editField = function( el, str ) {
+                var $input = view.$el.find( el ),
+                    $btn = $input.next( '.js-enable-edit' );
+
+                expect( document.activeElement ).not.toEqual( $input[0] );
+                expect( $input ).not.toHaveClass( 'edit' );
+                expect( $input ).toHaveClass( 'is-hidden' );
+
+                $btn.click();
+
+                $input.val( str );
+
+                // Enable editing
+                expect( $input ).not.toHaveClass( 'is-hidden' );
+                expect( $input ).toHaveClass( 'edit' );
+            };
+
+            keyPress = function( $el, key ) {
+                $el.trigger({
+                    type: 'keydown',
+                    keyCode: key,
+                    which: key,
+                    charCode: key
+                });
+            };
+
+            openPublishModal = function() {
+                var $publishBtn = view.$el.find('.js-publish-program'),
+                    defaultStatus = programData.status,
+                    publishedStatus = 'active';
+
+                expect( view.modalView ).not.toBeDefined();
+                expect( view.model.get( 'status' ) ).toEqual( defaultStatus );
+                expect( view.model.get( 'status' ) ).not.toEqual( publishedStatus );
+
+                $publishBtn.click();
+            };
+
+            resetCourseCodes = function() {
+                var originalRun = programData.course_codes[0];
+
+                programData.course_codes = [originalRun];
+            };
+
+            testUnchangedFieldBlur = function( el ) {
+                var $input = view.$el.find( el ),
+                    $btn = view.$el.find( '.js-add-course' ),
+                    title = $input.val(),
+                    update = title;
+
+                editField( el, update );
+                $btn.focus();
+                $input.blur();
+
+                expect( title ).toEqual( update );
+                expect( view.model.save ).not.toHaveBeenCalled();
+            };
+
+            testUpdatedFieldBlur = function( el, update ) {
+                var $input = view.$el.find( el ),
+                    $btn = view.$el.find( '.js-add-course' );
+
+                expect( $input.val() ).not.toEqual( update );
+
+                editField( el, update );
+
+                $btn.focus();
+                $input.blur();
+
+                expect( $input.val() ).toEqual( update );
+                expect( view.model.save ).toHaveBeenCalled();
+            };
+
+            testHidingButtonsAfterPublish = function( el ) {
+                expect( view.$el.find( el ).length ).toBeGreaterThan( 0 );
+                view.model.set({ status: 'active' });
+                view.render();
+                expect( view.$el.find( el ).length ).toEqual( 0 );
+            };
+
+            testInvalidUpdate = function( el, update ) {
+                var $input = view.$el.find( el ),
+                    $btn = view.$el.find( '.js-add-course' );
+
+                editField( el, update );
+
+                $btn.focus();
+                $input.blur();
+
+                expect( $input ).toHaveClass( errorClass );
+                expect( view.model.save ).not.toHaveBeenCalled();
+            };
+
+            beforeEach( function() {
+                // Set the DOM
+                setFixtures( '<div class="js-program-admin"></div>' );
+
+                jasmine.clock().install();
+
+                spyOn( ProgramModel.prototype, 'set' ).and.callThrough();
+                spyOn( ProgramModel.prototype, 'save' );
+                spyOn( CourseRunsCollection.prototype, 'fetch' );
+
+                model = new ProgramModel();
+                model.set( programData );
+
+                view = new ProgramDetailsView({
+                    model: model
+                });
+
+                view.courseRuns.set( courseRunsList );
+                view.courseRuns.parse({ results: courseRunsList });
+            });
+
+            afterEach( function() {
+                resetCourseCodes();
+                view.undelegateEvents();
+                view.remove();
+
+                jasmine.clock().uninstall();
+            });
+
+            describe( 'View data', function() {
+                it( 'should exist', function () {
+                    expect( view ).toBeDefined();
+                });
+
+                it( 'should render all of the run_modes from the model', function() {
+                   var $runs = view.$el.find('.js-course-runs'),
+                       domLength = $runs.find('.js-remove-run').length,
+                       objLength = programData.course_codes[0].run_modes.length;
+
+                    expect( domLength ).toEqual( objLength );
+                });
+            });
+
+            describe( 'Delete data', function() {
+                it( 'should remove a course when the delete button is clicked', function() {
+                    var $el = view.$el.find('.js-course-list'),
+                        $removeRunBtn = $el.find('.js-remove-course').first(),
+                        count = programData.course_codes.length;
+
+                    expect( $el.find('.js-remove-course').length ).toEqual( count );
+                    $removeRunBtn.click();
+
+                    setTimeout( function() {
+                        expect( $el.find('.js-remove-course').length ).toEqual( count - 1 );
+                    }, testTimeoutInterval );
+
+                    jasmine.clock().tick( testTimeoutInterval + 1 );
+                });
+
+                it( 'should remove a course run when the delete button is clicked', function() {
+                    var $runs = view.$el.find('.js-course-runs'),
+                        $removeRunBtn = $runs.find('.js-remove-run').first(),
+                        count = programData.course_codes[0].run_modes.length;
+
+                    expect( $runs.find('.js-remove-run').length ).toEqual( count );
+                    $removeRunBtn.click();
+
+                    setTimeout( function() {
+                        expect( $runs.find('.js-remove-run').length ).toEqual( count - 1 );
+                    }, testTimeoutInterval );
+
+                    jasmine.clock().tick( testTimeoutInterval + 1 );
+                });
+
+                it( 'should not show the delete course button if program status is not unpublished', function() {
+                    testHidingButtonsAfterPublish('.js-remove-course');
+                });
+
+                it( 'should not show the add course button if program status is not unpublished', function() {
+                    testHidingButtonsAfterPublish('.js-add-course');
+                });
+
+                it( 'should not show the delete run button if program status is not unpublished', function() {
+                    testHidingButtonsAfterPublish('.js-remove-run');
+                });
+            });
+
+            describe( 'Add data', function() {
+                it( 'should add a new course details view on click of the add course button', function() {
+                    var $btn = view.$el.find('.js-add-course').first();
+
+                    expect( view.$('.js-course-form').length ).toEqual( 0 );
+                    $btn.click();
+                    expect( view.$('.js-course-form').length ).toEqual( 1 );
+                });
+
+                it( 'should add a course when the form is submitted', function() {
+                    addCourse();
+                });
+
+                it( 'should not submit the course form when it is incomplete', function() {
+                    var $addCourseBtn = view.$el.find('.js-add-course').first(),
+                        $form,
+                        $submitBtn;
+
+                    expect( view.$el.find('.course-details').length ).toEqual( 1 );
+                    $addCourseBtn.click();
+                    $form = view.$('.js-course-form');
+                    expect( $form.find('.field-message.has-error').length ).toEqual( 0 );
+                    $submitBtn = $form.find('.js-select-course');
+                    $submitBtn.click();
+                    expect( $form.find('.field-message.has-error').length ).toEqual( 2 );
+                });
+
+                it( 'should allow a user to add a course run', function() {
+                    var runSelect = '.js-course-run-select',
+                        $runSelect,
+                        savedRunCount = programData.course_codes[0].run_modes.length;
+
+                    addCourse();
+                    expect( view.$(runSelect).length ).toEqual(0);
+                    view.$('.js-add-course-run').first().click();
+                    
+                    $runSelect = view.$(runSelect);
+                    expect( $runSelect.length ).toEqual(1);
+                    expect( view.$('.js-remove-run').length ).toEqual(savedRunCount);
+                    dropdownSelect($runSelect, courseRunsList[0].id);
+                    expect( view.$(runSelect).length ).toEqual(0);
+                    expect( view.$('.js-remove-run').length ).toEqual(savedRunCount + 1);
+                });
+
+                it( 'should update the course run dropdown if multiple are open and one is selected', function() {
+                    var runSelect = '.js-course-run-select',
+                        $addRunBtn,
+                        $courseView,
+                        courseRunOptionsCount = courseRunsList.length + 1;
+
+                    addCourse();
+                    expect( view.$(runSelect).length ).toEqual(0);
+
+                    $courseView = view.$('.course-container').last();
+                    $addRunBtn = $courseView.find('.js-add-course-run');
+                    $addRunBtn.click();
+                    
+                    expect( view.$(runSelect).length ).toEqual(1);
+                    expect( view.$(runSelect).find('option').length ).toEqual(courseRunOptionsCount);
+
+                    $addRunBtn.click();
+                    expect( view.$(runSelect).length ).toEqual(2);
+
+                    dropdownSelect(view.$(runSelect).first(), courseRunsList[0].id);
+                    expect( view.$(runSelect).length ).toEqual(1);
+                    expect( view.$(runSelect).find('option').length ).toEqual(courseRunOptionsCount - 1);
+                });
+            });
+
+            describe( 'Edit data', function() {
+                it( 'should enable a user to edit the name, subtitle and marketing slug fields', function() {
+                    editField( '.program-name', 'name' );
+                    editField( '.program-subtitle', 'subtitle' );
+                    editField( '.program-marketing-slug', 'marketing-slug' );
+                });
+
+                it( 'should not send an API call if a user does not change the value of an editable field', function() {
+                    testUnchangedFieldBlur( '.program-name' );
+                    testUnchangedFieldBlur( '.program-subtitle' );
+                    testUnchangedFieldBlur( '.program-marketing-slug' );
+                });
+
+                it( 'should send an API call if a user changes the value of an editable field', function() {
+                    testUpdatedFieldBlur( '.program-name',  'new-title' );
+                    testUpdatedFieldBlur( '.program-subtitle',  'new-subtitle' );
+                    testUpdatedFieldBlur( '.program-marketing-slug',  'new-marketing-slug' );
+                });
+
+                it( 'should show error messaging if the updated required field is empty', function() {
+                    testInvalidUpdate( '.program-name',  '' );
+                });
+
+                it( 'should show error messaging if the updated field value is too long', function() {
+                    var chars256 = 'x'.repeat(256);
+
+                    testInvalidUpdate( '.program-name',  chars256 );
+                    testInvalidUpdate( '.program-subtitle',  chars256 );
+                    testInvalidUpdate( '.program-marketing-slug',  chars256 );
+                });
+
+                it( 'should create a POST config object by default', function() {
+                    var config = view.model.getConfig();
+
+                    expect( config.type ).toEqual( 'POST' );
+                    expect( config.contentType ).toEqual( 'application/json' );
+                    expect( config.data ).not.toBeDefined();
+                });
+
+                it( 'should create a PATCH config object when passed in object sets patch as true', function() {
+                    var data = { name: 'patched name' },
+                        config = view.model.getConfig({
+                            patch: true,
+                            update: data
+                        });
+
+                    expect( config.type ).toEqual( 'PATCH' );
+                    expect( config.contentType ).toEqual( 'application/merge-patch+json' );
+                    expect( config.data ).toBeDefined();
+                    expect( config.data ).toEqual( JSON.stringify( data ) );
+                });
+            });
+
+            describe( 'Publish a Program', function() {
+                it( 'should open the publish modal when the publish button is clicked', function() {
+                    openPublishModal();
+                    expect( view.modalView ).toBeDefined();
+                });
+
+                it( 'should publish a program when the publish confirm button is clicked', function() {
+                    var defaultStatus = programData.status,
+                        publishedStatus = 'active';
+
+                    openPublishModal();
+                    expect( view.modalView ).toBeDefined();
+
+                    view.$el.find('.js-confirm').click();
+
+                    // Model should be set and save called
+                    expect( view.model.set ).toHaveBeenCalled();
+                    expect( view.model.get( 'status' ) ).not.toEqual( defaultStatus );
+                    expect( view.model.get( 'status' ) ).toEqual( publishedStatus );
+                    expect( view.model.save ).toHaveBeenCalled();
+
+                    // Publish button should be removed once API has completed its call
+                    expect( view.$el.find('.js-publish-program').length ).toEqual( 1 );
+                    view.model.trigger( 'sync' );
+                    expect( view.$el.find('.js-publish-program').length ).toEqual( 0 );
+                });
+
+                it( 'should show a validation error when publish button pressed if validation fails', function() {
+                    var $input = view.$el.find( '#program-marketing-slug' );
+
+                    view.model.set('marketing_slug', '');
+                    openPublishModal();
+                    expect( view.modalView ).not.toBeDefined();
+                    expect( $input ).toHaveClass( errorClass );
+                });
+
+                it( 'should destroy the publish modal when the cancel button is clicked', function() {
+                    openPublishModal();
+                    expect( view.modalView ).toBeDefined();
+
+                    // Close the modal
+                    view.$el.find('.js-cancel').click();
+
+                    // Expect the modal DOM elements to not be there anymore
+                    expect( view.$el.find('.js-cancel').length ).toEqual( 0 );
+                    expect( view.modalView.$parentEl.html().length ).toEqual( 0 );
+                });
+
+                it( 'should destroy the publish modal when the esc key is pressed', function() {
+                    openPublishModal();
+                    expect( view.modalView ).toBeDefined();
+
+                    // Close the modal
+                    keyPress( view.modalView.$el, constants.keyCodes.esc );
+
+                    // Expect the modal DOM elements to not be there anymore
+                    expect( view.$el.find('.js-cancel').length ).toEqual( 0 );
+                    expect( view.modalView.$parentEl.html().length ).toEqual( 0 );
+                });
+            });
+        });
+    }
+);
diff --git a/cms/static/sass/_base-v2.scss b/cms/static/sass/_base-v2.scss
new file mode 100644
index 00000000000..14a6c676a10
--- /dev/null
+++ b/cms/static/sass/_base-v2.scss
@@ -0,0 +1,15 @@
+// studio - base styling
+// ====================
+html {
+  height: 102%; // force scrollbar to prevent jump when scroll appears, cannot use overflow because it breaks drag
+}
+
+body {
+  min-width: $fg-min-width;
+  background: $gray-l5;
+  color: $gray-d2;
+}
+
+footer.primary{
+  font-size: font-size(x-small);
+}
diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss
index a8dd687fb67..d86b59869b5 100644
--- a/cms/static/sass/_base.scss
+++ b/cms/static/sass/_base.scss
@@ -590,35 +590,6 @@ hr.divide {
   }
 }
 
-.tooltip {
-  @include transition(opacity $tmg-f3 ease-out 0s);
-  @include font-size(12);
-  @extend %t-regular;
-  @extend %ui-depth5;
-  position: absolute;
-  top: 0;
-  left: 0;
-  padding: 0 10px;
-  border-radius: 3px;
-  background: rgba(0, 0, 0, 0.85);
-  line-height: 26px;
-  color: $white;
-  pointer-events: none;
-  opacity: 0.0;
-
-  &:after {
-    @include font-size(20);
-    content: 'â–¾';
-    display: block;
-    position: absolute;
-    bottom: -14px;
-    left: 50%;
-    margin-left: -7px;
-    color: rgba(0, 0, 0, 0.85);
-  }
-}
-
-
 // +Utility - Basic
 // ====================
 
diff --git a/cms/static/sass/_build-v1.scss b/cms/static/sass/_build-v1.scss
index 36c86d4d340..e65d9ee191b 100644
--- a/cms/static/sass/_build-v1.scss
+++ b/cms/static/sass/_build-v1.scss
@@ -45,6 +45,7 @@
 @import 'elements/modal-window';
 @import 'elements/uploaded-assets'; // layout for asset tables
 @import 'elements/creative-commons';
+@import 'elements/tooltip';
 
 // +Base - Specific Views
 // ====================
diff --git a/cms/static/sass/_build-v2.scss b/cms/static/sass/_build-v2.scss
index 2a032042f63..7335783fbd7 100644
--- a/cms/static/sass/_build-v2.scss
+++ b/cms/static/sass/_build-v2.scss
@@ -7,3 +7,13 @@
 @import 'config';
 
 // Extensions
+@import 'partials/variables';
+@import 'mixins-v2';
+@import 'base-v2';
+@import 'elements-v2/controls';
+@import 'elements-v2/header';
+@import 'elements-v2/navigation';
+@import 'elements/footer';
+@import 'elements-v2/sock';
+@import 'elements-v2/tooltip';
+@import 'programs/build';
diff --git a/cms/static/sass/_mixins-v2.scss b/cms/static/sass/_mixins-v2.scss
new file mode 100644
index 00000000000..86031b4c7ef
--- /dev/null
+++ b/cms/static/sass/_mixins-v2.scss
@@ -0,0 +1,4 @@
+// pill button
+%ui-btn-pill {
+  border-radius: ($baseline/5);
+}
diff --git a/cms/static/sass/elements-v2/_controls.scss b/cms/static/sass/elements-v2/_controls.scss
new file mode 100644
index 00000000000..69116366ad4
--- /dev/null
+++ b/cms/static/sass/elements-v2/_controls.scss
@@ -0,0 +1,45 @@
+// +UI Dropdown Button - Extend
+// ====================
+%ui-btn-dd {
+  @extend %ui-btn;
+  @extend %ui-btn-pill;
+  padding:($baseline/4) ($baseline/2);
+  border-width: 1px;
+  border-style: solid;
+  border-color: transparent;
+  text-align: center;
+
+  &:hover, &:active {
+    @extend %ui-fake-link;
+    border-color: $gray-l3;
+  }
+
+  &.current, &.active, &.is-selected {
+    box-shadow: inset 0 1px 2px 1px $shadow-l1;
+    border-color: $gray-l3;
+  }
+}
+
+// +UI Nav Dropdown Button - Extend
+// ====================
+%ui-btn-dd-nav-primary {
+  @extend %ui-btn-dd;
+  background: $white;
+  border-color: $white;
+  color: $gray-d1;
+
+  &:hover, &:active {
+    background: $white;
+    color: $blue-s1;
+  }
+
+  &.current, &.active {
+    background: $white;
+    color: $gray-d4;
+
+    &:hover, &:active {
+      color: $blue-s1;
+    }
+  }
+}
+
diff --git a/cms/static/sass/elements-v2/_header.scss b/cms/static/sass/elements-v2/_header.scss
new file mode 100644
index 00000000000..f04a3d93c91
--- /dev/null
+++ b/cms/static/sass/elements-v2/_header.scss
@@ -0,0 +1,172 @@
+// studio - elements - global header
+// ====================
+
+.wrapper-header {
+  position: relative;
+  width: 100%;
+  box-shadow: 0 1px 2px 0 $shadow-l1;
+  margin: 0;
+  padding: 0 $baseline;
+  background: $white;
+
+  header.primary {
+    @include clearfix();
+    @include span(12);
+    @include float(none);
+    box-sizing: border-box;
+    max-width: $fg-max-width;
+    min-width: $fg-min-width;
+    margin: 0 auto;
+  }
+
+  // ====================
+
+  // basic layout
+  .wrapper-l, .wrapper-r {
+    background: $white;
+  }
+
+  .wrapper-l {
+    @include span(7);
+  }
+
+  .wrapper-r {
+    @include span(4 last);
+    @include text-align(right);
+  }
+
+  .branding, .info-course, .nav-course, .nav-account, .nav-pitch {
+    box-sizing: border-box;
+    display: inline-block;
+    vertical-align: middle;
+  }
+
+  .user-language-selector {
+    width: 120px;
+    display: inline-block;
+    margin: 0 10px 0 5px;
+    vertical-align: sub;
+
+    .language-selector {
+      width: 120px;
+    }
+  }
+
+  .nav-account {
+    width: auto;
+  }
+
+  // basic layout - nav items
+  .nav-dd {
+    .nav-item {
+      display: inline-block;
+      vertical-align: middle;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      .title{
+        @extend %ui-btn-dd-nav-primary;
+        @include transition(all $tmg-f2 ease-in-out 0s);
+        line-height: 16px;
+        margin-top: 6px;
+        font-size: font-size(base);
+        font-weight: font-weight(semi-bold);
+        .nav-sub .nav-item {
+          .icon {
+            display: inline-block;
+            vertical-align: middle;
+            margin-right: ($baseline/4);
+          }
+        }
+      }
+    }
+
+    .nav-item a {
+      color: $gray-d1;
+
+      &:hover,
+      &:focus {
+        color: $blue-s1;
+      }
+    }
+  }
+
+  // ====================
+
+  // specific elements - branding
+  .branding {
+    padding: ($baseline*0.75) 0;
+
+    .brand-link {
+      display: block;
+
+      .brand-image {
+        max-height: ($baseline*2);
+        display: block;
+      }
+    }
+  }
+
+  // ====================
+
+  // specific elements - account-based nav
+  .nav-account {
+    position: relative;
+    padding: ($baseline*0.75) 0;
+
+    .nav-sub {
+      @include text-align(left);
+    }
+
+    .nav-account-help {
+
+      .wrapper-nav-sub {
+        width: ($baseline*10);
+      }
+    }
+
+    .nav-account-user {
+
+      .title {
+        max-width: ($baseline*6.5);
+        display: inline-block;
+        max-width: 84%;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+
+    .settings-language-form {
+      margin-top: 4px;
+      .language-selector {
+        width: 130px;
+      }
+    }
+  }
+}
+// ====================
+
+// CASE: user signed in
+.is-signedin {
+
+  .wrapper-l {
+    width: flex-grid(8,12);
+  }
+
+  .wrapper-r {
+    width: flex-grid(4,12);
+  }
+
+  .branding {
+    @include margin-right(2%);
+  }
+
+  .nav-account {
+    top: ($baseline/4);
+  }
+}
+
+
diff --git a/cms/static/sass/elements-v2/_navigation.scss b/cms/static/sass/elements-v2/_navigation.scss
new file mode 100644
index 00000000000..ce4b43225cf
--- /dev/null
+++ b/cms/static/sass/elements-v2/_navigation.scss
@@ -0,0 +1,22 @@
+// skip navigation
+.nav-skip,
+.transcript-skip {
+  @include left(0);
+  font-size: font-size(small);
+  display: inline-block;
+  position: absolute;
+  top: -($baseline*30);
+  overflow: hidden;
+  background: $white;
+  border-bottom: 1px solid $gray-l4;
+  padding: ($baseline*0.75) ($baseline/2);
+
+  &:focus,
+  &:active {
+    position: relative;
+    top: auto;
+    width: auto;
+    height: auto;
+    margin: 0;
+  }
+}
diff --git a/cms/static/sass/elements-v2/_sock.scss b/cms/static/sass/elements-v2/_sock.scss
new file mode 100644
index 00000000000..c34daf795a4
--- /dev/null
+++ b/cms/static/sass/elements-v2/_sock.scss
@@ -0,0 +1,135 @@
+// studio - elements - support sock
+// ====================
+
+.wrapper-sock {
+  @include clearfix();
+  position: relative;
+  margin: ($baseline*2) 0 0 0;
+  border-top: 1px solid $gray-l4;
+  width: 100%;
+
+  .wrapper-inner {
+    @include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%);
+    display: none;
+    width: 100% !important;
+    border-bottom: 1px solid $white;
+    padding: 0 $baseline !important;
+  }
+
+  // sock - actions
+  .list-cta {
+    @extend %ui-depth1;
+    position: absolute;
+    top: -($baseline*0.75);
+    width: 100%;
+    margin: 0 auto;
+    text-align: center;
+
+    .cta-show-sock {
+      @extend %ui-btn-pill;
+      background: $gray-l5;
+      font-size: font-size(x-small);
+      padding: ($baseline/2) $baseline;
+      color: $gray;
+
+      .icon {
+        @include margin-right($baseline/4);
+      }
+
+      &:hover {
+        background: $blue;
+        color: $white;
+      }
+    }
+  }
+
+  // sock - additional help
+  .sock {
+    @include clearfix();
+    @include span(12);
+    max-width: $fg-max-width;
+    min-width: $fg-min-width;
+    margin: 0 auto;
+    padding: ($baseline*2) 0;
+    color: $gray-l3;
+
+    // shared elements
+    .support,
+    .feedback {
+      box-sizing: border-box;
+
+      .title {
+        color: $white;
+        margin-bottom: ($baseline/2);
+      }
+
+      .copy {
+        margin: 0 0 $baseline 0;
+      }
+
+      .list-actions {
+        list-style: none;
+
+        .action-item {
+          @include float(left);
+          @include margin-right($baseline/2);
+          margin-bottom: ($baseline/2);
+
+          &:last-child {
+            @include margin-right(0);
+          }
+
+          .action {
+            display: block;
+
+            .icon {
+              vertical-align: middle;
+              @include margin-right($baseline/4);
+            }
+          }
+
+          .tip {
+            @extend .sr-only;
+          }
+        }
+
+        .action-primary {
+          @extend %btn-brand;
+          @extend %btn-small;
+        }
+      }
+    }
+
+    // studio support content
+    .support {
+      @include float(left);
+      @include span(8);
+      margin-right: flex-gutter();
+
+      .action-item {
+        width: flexgrid(4,8);
+      }
+    }
+
+    // studio feedback content
+    .feedback {
+      @include float(left);
+      @include span(4);
+      .action-item {
+        width: flexgrid(4,4);
+      }
+    }
+  }
+
+  // case: sock content is shown
+  &.is-shown {
+    border-color: $gray-d3;
+
+    .list-cta .cta-show-sock {
+      background: $gray-d3;
+      border-color: $gray-d3;
+      color: $white;
+      font-size: font-size(small);
+    }
+  }
+}
diff --git a/cms/static/sass/elements-v2/_tooltip.scss b/cms/static/sass/elements-v2/_tooltip.scss
new file mode 100644
index 00000000000..ef826da6441
--- /dev/null
+++ b/cms/static/sass/elements-v2/_tooltip.scss
@@ -0,0 +1,25 @@
+.tooltip {
+  @include transition(opacity $tmg-f3 ease-out 0s);
+  position: absolute;
+  top: 0;
+  left: 0;
+  padding: 0 10px;
+  border-radius: 3px;
+  background: $black-t4;
+  line-height: 26px;
+  font-size: font-size(x-small);
+  color: $white;
+  pointer-events: none;
+  opacity: 0;
+
+  &:after {
+    font-size: font-size(x-large);
+    content: 'â–¾';
+    display: block;
+    position: absolute;
+    bottom: -14px;
+    left: 50%;
+    margin-left: -7px;
+    color: $black-t4;
+  }
+}
diff --git a/cms/static/sass/elements/_tooltip.scss b/cms/static/sass/elements/_tooltip.scss
new file mode 100644
index 00000000000..77c42d63de3
--- /dev/null
+++ b/cms/static/sass/elements/_tooltip.scss
@@ -0,0 +1,27 @@
+.tooltip {
+  @include transition(opacity $tmg-f3 ease-out 0s);
+  @include font-size(12);
+  @extend %t-regular;
+  @extend %ui-depth5;
+  position: absolute;
+  top: 0;
+  left: 0;
+  padding: 0 10px;
+  border-radius: 3px;
+  background: rgba(0, 0, 0, 0.85);
+  line-height: 26px;
+  color: $white;
+  pointer-events: none;
+  opacity: 0.0;
+
+  &:after {
+    @include font-size(20);
+    content: 'â–¾';
+    display: block;
+    position: absolute;
+    bottom: -14px;
+    left: 50%;
+    margin-left: -7px;
+    color: rgba(0, 0, 0, 0.85);
+  }
+}
\ No newline at end of file
diff --git a/cms/static/sass/partials/_variables.scss b/cms/static/sass/partials/_variables.scss
index 4e9f1dac9db..44931cdb0ca 100644
--- a/cms/static/sass/partials/_variables.scss
+++ b/cms/static/sass/partials/_variables.scss
@@ -46,6 +46,7 @@ $black-t0: rgba($black, 0.125);
 $black-t1: rgba($black, 0.25);
 $black-t2: rgba($black, 0.5);
 $black-t3: rgba($black, 0.75);
+$black-t4: rgba($black, 0.85);
 
 $white: rgb(255,255,255);
 $white-t0: rgba($white, 0.125);
diff --git a/cms/static/sass/programs/_app-container.scss b/cms/static/sass/programs/_app-container.scss
new file mode 100644
index 00000000000..b53b3291267
--- /dev/null
+++ b/cms/static/sass/programs/_app-container.scss
@@ -0,0 +1,10 @@
+
+// ------------------------------
+// Programs: App Container
+
+// About: styling for setting up the wrapper.
+.program-app {
+  &.layout-1q3q {
+    max-width: 1250px;
+  }
+}
diff --git a/cms/static/sass/programs/_build.scss b/cms/static/sass/programs/_build.scss
new file mode 100644
index 00000000000..272dcbe63fd
--- /dev/null
+++ b/cms/static/sass/programs/_build.scss
@@ -0,0 +1,9 @@
+// ------------------------------
+// Programs: Main Style Compile
+
+// About: Sass compile for the Programs IDA.
+
+@import 'components';
+@import 'views';
+@import 'modals';
+@import 'app-container';
diff --git a/cms/static/sass/programs/_components.scss b/cms/static/sass/programs/_components.scss
new file mode 100644
index 00000000000..b59e018bb93
--- /dev/null
+++ b/cms/static/sass/programs/_components.scss
@@ -0,0 +1,99 @@
+// ------------------------------
+// Programs: Components
+
+// About: styling for specific UI components ranging from global to modular.
+
+// #BUTTONS
+// #FORMS
+
+
+// ------------------------------
+// #BUTTONS
+// ------------------------------
+.btn {
+  &.btn-delete,
+  &.btn-edit {
+    border: none;
+    background: none;
+    color: palette(grayscale, base);
+
+    &:hover,
+    &:focus,
+    &:active {
+      color: palette(grayscale, black);
+    }
+  }
+
+  &.full {
+    width: 100%;
+  }
+
+  &.right {
+    @include float(right);
+  }
+
+  &.btn-create {
+    background: palette(success, base);
+    border-color: palette(success, base);
+
+    // STATE: hover and focus
+    &:hover,
+    &.is-hovered,
+    &:focus,
+    &.is-focused {
+      background: shade($success, 33%);
+      color: $btn-default-focus-color;
+    }
+
+    // STATE: is pressed or active
+    &:active,
+    &.is-pressed,
+    &.is-active {
+      border-color: shade($success, 33%);
+      background: shade($success, 33%);
+    }
+
+    .text {
+      margin-left: 5px;
+    }
+  }
+
+  .icon,
+  .text {
+    vertical-align: middle;
+  }
+
+  .icon {
+    font-size: 16px;
+  }
+}
+
+// ------------------------------
+// #FORMS
+// ------------------------------
+.field {
+  .invalid {
+    border: 2px solid palette(error, base);
+  }
+
+  .field-input,
+  .field-hint,
+  .field-message {
+    min-with: 300px;
+    width: 50%;
+
+    &.is-hidden {
+      @extend .is-hidden;
+    }
+  }
+
+  .copy {
+    vertical-align: middle;
+  }
+}
+
+.form-group {
+  &.bg-white {
+    background-color: palette(grayscale, white);
+  }
+}
diff --git a/cms/static/sass/programs/_modals.scss b/cms/static/sass/programs/_modals.scss
new file mode 100644
index 00000000000..890e609dccc
--- /dev/null
+++ b/cms/static/sass/programs/_modals.scss
@@ -0,0 +1,76 @@
+// ------------------------------
+// Programs: Modals
+
+// About: styling for modals.
+.modal-window-overlay {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  background-color: palette(grayscale-cool, x-dark);
+  opacity: 0.5;
+  z-index: 1000;
+}
+
+.modal-window {
+  position: absolute;
+  background-color: palette(grayscale, black);
+  width: 80%;
+  left: 10%;
+  top: 40%;
+  z-index: 1001;
+}
+
+.modal-content {
+  margin: 5px;
+  padding: 20px;
+  background-color: palette(grayscale-cool, x-dark);
+  border-top: 5px solid palette(warning, base);
+
+  .copy {
+    color: palette(grayscale, white);
+  }
+
+  .emphasized {
+    color: palette(grayscale, white-t);
+    font-weight: font-weight(bold);
+  }
+}
+
+.modal-actions {
+  padding: 10px 20px;
+
+  .btn {
+    color: palette(grayscale, white-t);
+  }
+
+  .btn-brand {
+    background: palette(warning, base);
+    border-color: palette(warning, base);
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: palette(warning, dark);
+      border-color: palette(warning, dark);;
+    }
+  }
+
+  .btn-neutral {
+    background: transparent;
+    border-color: transparent;
+    &:hover,
+    &:focus,
+    &:active {
+      border-color: palette(grayscale-cool, light)
+    }
+  }
+}
+
+@include breakpoint( $bp-screen-sm ) {
+  .modal-window {
+    width: 440px;
+    left: calc( 50% - 220px );
+  }
+}
diff --git a/cms/static/sass/programs/_views.scss b/cms/static/sass/programs/_views.scss
new file mode 100644
index 00000000000..a4b9f282fe4
--- /dev/null
+++ b/cms/static/sass/programs/_views.scss
@@ -0,0 +1,62 @@
+
+// ------------------------------
+// Programs: Views
+
+// About: styling for specific views.
+
+// ------------------------------
+// #PROGRAM LISTS
+// ------------------------------
+.program-list {
+  list-style-type: none;
+  padding-left: 0;
+
+  .program-details {
+    .name {
+      font-size: 2rem;
+    }
+
+    .status {
+      @include float(right);
+    }
+
+    .category {
+      color: palette(grayscale, base);
+    }
+  }
+}
+
+.app-header {
+  @include clearfix();
+  border-bottom: 1px solid palette(grayscale, base);
+  margin-bottom: 20px;
+}
+
+.course-container {
+  .subtitle {
+    color: palette(grayscale, base);
+  }
+}
+
+.run-container {
+  position: relative;
+  margin: {
+    bottom: 20px;
+  };
+
+  &:before {
+    content: '';
+    width: 5px;
+    height: calc( 100% + 1px );
+    background: palette(grayscale, base);
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+}
+
+.course-container {
+  margin: {
+    bottom: 20px;
+  };
+}
diff --git a/cms/static/sass/studio-main-v2.scss b/cms/static/sass/studio-main-v2.scss
index cd355d5a7d1..f31bcb3817b 100644
--- a/cms/static/sass/studio-main-v2.scss
+++ b/cms/static/sass/studio-main-v2.scss
@@ -12,3 +12,4 @@ $pattern-library-path: '../edx-pattern-library' !default;
 
 // Load the shared build
 @import 'build-v2';
+@import 'programs/build';
diff --git a/cms/templates/js/programs/confirm_modal.underscore b/cms/templates/js/programs/confirm_modal.underscore
new file mode 100644
index 00000000000..cafe8978376
--- /dev/null
+++ b/cms/templates/js/programs/confirm_modal.underscore
@@ -0,0 +1,24 @@
+<div class="wrapper wrapper-modal-window wrapper-modal-window-<%- name %>"
+     aria-describedby="modal-window-description"
+     aria-labelledby="modal-window-title"
+     aria-hidden=""
+     role="dialog">
+    <div class="modal-window-overlay"></div>
+    <div class="js-focus-first modal-window modal-medium modal-type-confirm" tabindex="-1" aria-labelledby="modal-window-title">
+        <div class="<%- name %>-modal">
+            <div class="modal-content">
+                <span class="copy copy-lead emphasized"><%- title %></span>
+                <p class="copy copy-base"><%- body %></p>
+            </div>
+            <div class="modal-actions">
+                <h3 class="sr-only"><%- gettext('Actions') %></h3>
+                <button class="js-confirm btn btn-brand btn-base">
+                    <span><%- cta.confirm %></span>
+                </button>
+                <button class="js-cancel btn btn-neutral btn-base">
+                    <span><%- cta.cancel %></span>
+                </button>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/cms/templates/js/programs/course_details.underscore b/cms/templates/js/programs/course_details.underscore
new file mode 100644
index 00000000000..42aa7684e09
--- /dev/null
+++ b/cms/templates/js/programs/course_details.underscore
@@ -0,0 +1,49 @@
+<div class="card course-container">
+<% if ( display_name ) {  %>
+    <span class="copy copy-large emphasized"><%- display_name %></span>
+  <% if ( status === 'unpublished' ) { %>
+    <button class="js-remove-course btn btn-delete right" data-tooltip="<%- gettext('Delete course') %>">
+        <span class="icon fa fa-trash-o" aria-hidden="true"></span>
+        <span class="sr-only"><%- interpolate(
+            gettext('Remove %(name)s from the program'),
+            { name: display_name },
+            true
+        ) %></span>
+    </button>
+  <% } %>
+    <p class="copy copy-base subtitle"><%- organization.display_name %> / <%- key %>
+    <div class="js-course-runs"></div>
+  <% if ( courseRuns.length > -1 ) { %>
+    <button class="js-add-course-run btn btn-neutral btn-base full">
+        <span class="icon fa fa-plus" aria-hidden="true"></span>
+        <span class="text"><%- gettext('Add another run') %></span>
+    </button>
+  <% } %>
+<% } else { %>
+    <form class="form js-course-form">
+        <fieldset class="form-group">
+            <div class="field">
+                <label class="field-label" for="course-key-<%- cid %>"><%- gettext('Course Code') %></label>
+                <input id="course-key-<%- cid %>" class="field-input input-text course-key" name="key" aria-describedby="course-key-<%- cid %>-desc" maxlength="255" required>
+                <div class="field-message">
+                    <span class="field-message-content"></span>
+                </div>
+                <div class="field-hint" id="course-key-<%- cid %>-desc">
+                    <p><%- gettext('The unique number that identifies your course within your organization, e.g. CS101.') %></p>
+                </div>
+            </div>
+            <div class="field">
+                <label class="field-label" for="display-name-<%- cid %>"><%- gettext('Course Title') %></label>
+                <input id="display-name-<%- cid %>" class="field-input input-text display-name" name="display_name" aria-describedby="display-name-<%- cid %>-desc" maxlength="255" required>
+                <div class="field-message">
+                    <span class="field-message-content"></span>
+                </div>
+                <div class="field-hint" id="display-name-<%- cid %>-desc">
+                    <p><%- gettext('The title entered here will override the title set for the individual run of the course. It will be displayed on the XSeries progress page and in marketing presentations.') %></p>
+                </div>
+            </div>
+            <button class="btn btn-primary js-select-course"><%- gettext('Save Course') %></button>
+        </fieldset>
+    </form>
+<% } %>
+</div>
diff --git a/cms/templates/js/programs/course_run.underscore b/cms/templates/js/programs/course_run.underscore
new file mode 100644
index 00000000000..11264a8f96e
--- /dev/null
+++ b/cms/templates/js/programs/course_run.underscore
@@ -0,0 +1,36 @@
+<div class="card run-container">
+  <% if ( !_.isUndefined(course_key) ) { %>
+    <span class="copy copy-large emphasized"><%- interpolate(
+        gettext('Run %(key)s'),
+        { key: course_key },
+        true
+    ) %></span>
+    <% if ( programStatus === 'unpublished' ) { %>
+        <button class="js-remove-run btn btn-delete right" data-tooltip="<%- gettext('Delete course run') %>">
+            <span class="icon fa fa-trash-o" aria-hidden="true"></span>
+            <span class="sr-only"><%- interpolate(
+                gettext('Remove run %(key)s from the program'),
+                { key: course_key },
+                true
+            ) %></span>
+        </button>
+    <% } %>
+    <div class="copy copy-base subtitle"><%- interpolate(
+        gettext('Start Date: %(date)s'),
+        { date: start_date },
+        true
+    ) %></div>
+    <div class="copy copy-base subtitle"><%- interpolate(
+        gettext('Mode: %(mode)s'),
+        { mode: mode_slug },
+        true
+    ) %></div>
+  <% } else { %>
+    <select class="js-course-run-select">
+        <option><%- gettext('Please select a Course Run') %></option>
+        <% _.each(courseRuns, function(run) { %>
+            <option value="<%- run.id %>"><%- run.name %>: <%- run.id %></option>
+        <% }); %>
+    </select>
+  <% } %>
+</div>
diff --git a/cms/templates/js/programs/program_creator_form.underscore b/cms/templates/js/programs/program_creator_form.underscore
new file mode 100644
index 00000000000..a4db9d6b5da
--- /dev/null
+++ b/cms/templates/js/programs/program_creator_form.underscore
@@ -0,0 +1,65 @@
+<h3 class="hd-3 emphasized"><%- gettext('Create a New Program') %></h3>
+<form class="form">
+    <fieldset class="form-group bg-white">
+        <div class="field">
+            <label class="field-label" for="program-type"><%- gettext('Program type') %></label>
+            <select id="program-type" class="field-input input-select program-type" name="category" disabled>
+                <option value="xseries"><%- gettext('XSeries') %></option>
+            </select>
+            <div class="field-message">
+                <span class="field-message-content"></span>
+            </div>
+        </div>
+
+        <div class="field">
+            <label class="field-label" for="program-org"><%- gettext('Organization') %></label>
+            <select id="program-org" class="field-input input-select program-org" name="organizations">
+                <option value="false"><%- gettext('Select an organization') %></option>
+              <% _.each( orgs, function( org ) { %>
+                <option value="<%- org.key %>"><%- org.display_name %></option>
+              <% }); %>
+            </select>
+            <div class="field-message">
+                <span class="field-message-content"></span>
+            </div>
+        </div>
+
+        <div class="field">
+            <label class="field-label" for="program-name"><%- gettext('Name') %></label>
+            <input id="program-name" class="field-input input-text program-name" name="name" maxlength="64" aria-describedby="program-name-desc" required>
+            <div class="field-message">
+                <span class="field-message-content"></span>
+            </div>
+            <div class="field-hint" id="program-name-desc">
+                <p><%- gettext('The public display name of the program.') %></p>
+            </div>
+        </div>
+
+        <div class="field">
+            <label class="field-label" for="program-subtitle"><%- gettext('Subtitle') %></label>
+            <input id="program-subtitle" class="field-input input-text program-subtitle" name="subtitle" maxlength="255" aria-describedby="program-subtitle-desc">
+            <div class="field-message">
+                <span class="field-message-content"></span>
+            </div>
+            <div class="field-hint" id="program-subtitle-desc">
+                <p><%- gettext('A short description of the program, including concepts covered and expected outcomes (255 character limit).') %></p>
+            </div>
+        </div>
+
+        <div class="field">
+            <label class="field-label" for="program-marketing-slug"><%- gettext('Marketing Slug') %></label>
+            <input id="program-marketing-slug" class="field-input input-text program-marketing-slug" name="marketing_slug" maxlength="255" aria-describedby="program-marketing-slug-desc">
+            <div class="field-message">
+                <span class="field-message-content"></span>
+            </div>
+            <div class="field-hint" id="program-marketing-slug-desc">
+                <p><%- gettext('Slug used to generate links to the marketing site.') %></p>
+            </div>
+        </div>
+
+        <div class="field">
+            <button class="btn btn-brand btn-base js-create-program"><%- gettext('Create') %></button>
+            <button class="btn btn-neutral btn-base js-abort-view"><%- gettext('Cancel') %></button>
+        </div>
+    </fieldset>
+</form>
diff --git a/cms/templates/js/programs/program_details.underscore b/cms/templates/js/programs/program_details.underscore
new file mode 100644
index 00000000000..bb606fb03b8
--- /dev/null
+++ b/cms/templates/js/programs/program_details.underscore
@@ -0,0 +1,63 @@
+<header class="app-header">
+    <form>
+        <div class="layout-col layout-col-b">
+            <div class="js-inline-edit field">
+                <span class="js-model-value copy copy-large emphasized"><%- name %></span>
+                <label for="program-name" class="sr-only"><%- gettext('Name') %></label>
+                <input type="text" value="<%- name %>" id="program-name" class="program-name field-input is-hidden" name="name" data-field="name" maxlength="64" required>
+                <button class="js-enable-edit btn btn-edit" data-tooltip="<%- gettext('Edit the program title') %>">
+                    <span class="icon fa fa-pencil" aria-hidden="true"></span>
+                    <span class="sr-only"><%- gettext('Edit the program\'s name.') %></span>
+                </button>
+                <div class="field-message">
+                    <span class="field-message-content"></span>
+                </div>
+            </div>
+
+            <div class="js-inline-edit field">
+                <span class="js-model-value copy copy-base subtitle"><%- subtitle %></span>
+                <label for="program-subtitle" class="sr-only"><%- gettext('Subtitle') %></label>
+                <input type="text" value="<%- subtitle %>" id="program-subtitle" class="program-subtitle field-input is-hidden" name="subtitle" data-field="subtitle" maxlength="255">
+                <button class="js-enable-edit btn btn-edit" data-tooltip="<%- gettext('Edit the program subtitle') %>">
+                    <span class="icon fa fa-pencil" aria-hidden="true"></span>
+                    <span class="sr-only"><%- gettext('Edit the program\'s subtitle.') %></span>
+                </button>
+                <div class="field-message">
+                    <span class="field-message-content"></span>
+                </div>
+            </div>
+
+            <div class="js-inline-edit field">
+                <span class="js-model-value copy copy-base subtitle"><%- marketing_slug %></span>
+                <label for="program-subtitle" class="sr-only"><%- gettext('Marketing Slug') %></label>
+                <input type="text" value="<%- marketing_slug %>" id="program-marketing-slug" class="program-marketing-slug field-input is-hidden" name="marketing_slug" data-field="marketing_slug" maxlength="255">
+                <button class="js-enable-edit btn btn-edit" data-tooltip="<%- gettext('Edit the program marketing slug') %>">
+                    <span class="icon fa fa-pencil" aria-hidden="true"></span>
+                    <span class="sr-only"><%- gettext('Edit the program\'s marketing slug.') %></span>
+                </button>
+                <div class="field-message">
+                    <span class="field-message-content"></span>
+                </div>
+            </div>
+        </div>
+
+        <div class="layout-col layout-col-a">
+          <% if ( status === 'unpublished' ) { %>
+            <button class="js-publish-program btn btn-neutral btn-base btn-grey right">
+                <span><%- gettext('Publish') %></span>
+            </button>
+          <% } %>
+        </div>
+    </form>
+</header>
+<div class="layout-col layout-col-b">
+    <div class="js-course-list"></div>
+  <% if ( status === 'unpublished' ) { %>
+    <button class="js-add-course btn btn-neutral btn-base full">
+        <span class="icon fa fa-plus" aria-hidden="true"></span>
+        <span class="text"><%- gettext('Add a course') %></span>
+    </button>
+  <% } %>
+</div>
+<aside class="js-aside layout-col layout-col-a"></aside>
+<div class="js-publish-modal"></div>
diff --git a/cms/templates/program_authoring.html b/cms/templates/program_authoring.html
index 4c7d2e8bf8a..0f15d942207 100644
--- a/cms/templates/program_authoring.html
+++ b/cms/templates/program_authoring.html
@@ -9,11 +9,11 @@
 <%block name="title">${_("Program Administration")}</%block>
 
 <%block name="header_extras">
-<link rel="stylesheet" href=${authoring_app_config.css_url}>
+<%! main_css = "style-main-v2" %>
 </%block>
 
 <%block name="requirejs">
-  require(['${authoring_app_config.js_url | n, js_escaped_string}'], function () {});
+  require(["js/programs/program_admin_app"], function () {});
 </%block>
 
 <%block name="content">
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html
index 2602adff51d..8e91d520163 100644
--- a/cms/templates/widgets/header.html
+++ b/cms/templates/widgets/header.html
@@ -10,9 +10,11 @@
   <header class="primary" role="banner">
 
     <div class="wrapper wrapper-l">
-      <h1 class="branding"><a href="/">
-        <img src="${static.url('images/studio-logo.png')}" alt="${settings.STUDIO_NAME}" />
-      </a></h1>
+      <h1 class="branding">
+        <a class="brand-link" href="/">
+          <img class="brand-image" src="${static.url('images/studio-logo.png')}" alt="${settings.STUDIO_NAME}" />
+        </a>
+      </h1>
 
       % if context_course:
       <%
@@ -215,33 +217,20 @@
       % endif
       % if user.is_authenticated():
       <nav class="nav-account nav-is-signedin nav-dd ui-right" aria-label="${_('Account')}">
-        <h2 class="sr">${_("Account Navigation")}</h2>
+        <h2 class="sr-only">${_("Account Navigation")}</h2>
         <ol>
           <li class="nav-item nav-account-help">
             <h3 class="title"><span class="label"><a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_('Contextual Online Help')}" target="_blank">${_("Help")}</a></span></h3>
           </li>
           <li class="nav-item nav-account-user">
-            <h3 class="title"><span class="label"><span class="label-prefix sr">${_("Currently signed in as:")}</span><span class="account-username" title="${ user.username }">${ user.username }</span></span> <span class="icon fa fa-caret-down ui-toggle-dd" aria-hidden="true"></span></h3>
-
-            <div class="wrapper wrapper-nav-sub">
-              <div class="nav-sub">
-                <ul>
-                  <li class="nav-item nav-account-dashboard">
-                    <a href="/">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
-                  </li>
-                  <li class="nav-item nav-account-signout">
-                    <a class="action action-signout" href="${reverse('logout')}">${_("Sign Out")}</a>
-                  </li>
-                </ul>
-              </div>
-            </div>
+            <%include file="user_dropdown.html" args="online_help_token=online_help_token" />
           </li>
         </ol>
       </nav>
 
-      % else:
+    % else:
       <nav class="nav-not-signedin nav-pitch" aria-label="${_('Account')}">
-        <h2 class="sr">${_("Account Navigation")}</h2>
+        <h2 class="sr-only">${_("Account Navigation")}</h2>
         <ol>
           <li class="nav-item nav-not-signedin-help">
             <a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_('Contextual Online Help')}" target="_blank">${_("Help")}</a>
@@ -254,7 +243,7 @@
           </li>
         </ol>
       </nav>
-      % endif
+    % endif
     </div>
   </header>
 </div>
diff --git a/cms/templates/widgets/sock.html b/cms/templates/widgets/sock.html
index 5c3cfc5a1b6..b271376a9ca 100644
--- a/cms/templates/widgets/sock.html
+++ b/cms/templates/widgets/sock.html
@@ -1,8 +1,8 @@
+<%page expression_filter="h" args="online_help_token" />
 <%!
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
 %>
-<%page args="online_help_token"/>
 <div class="wrapper-sock wrapper">
   <ul class="list-actions list-cta">
     <li class="action-item">
@@ -15,8 +15,7 @@ from django.core.urlresolvers import reverse
 
   <div class="wrapper-inner wrapper">
     <section class="sock" id="sock" aria-labelledby="sock-heading">
-        <h2 id="sock-heading" class="title sr">${_("{studio_name} Documentation").format(studio_name=settings.STUDIO_NAME)}</h2>
-
+        <h2 id="sock-heading" class="title sr-only">${_("{studio_name} Documentation").format(studio_name=settings.STUDIO_NAME)}</h2>
       <div class="support">
         <%!
         from django.conf import settings
diff --git a/cms/templates/widgets/user_dropdown.html b/cms/templates/widgets/user_dropdown.html
new file mode 100644
index 00000000000..bebc2e21d0c
--- /dev/null
+++ b/cms/templates/widgets/user_dropdown.html
@@ -0,0 +1,54 @@
+<%page expression_filter="h"/>
+<%namespace name='static' file='../static_content.html'/>
+<%!
+  from django.conf import settings
+  from django.core.urlresolvers import reverse
+  from django.utils.translation import ugettext as _
+%>
+
+% if uses_pattern_library:
+  <div class="wrapper-user-menu dropdown-menu-container logged-in js-header-user-menu">
+      <h3 class="title menu-title">
+          <span class="sr-only">${_("Currently signed in as:")}</span>
+          <span class="account-username" title="${ user.username }">${ user.username }</span>
+      </h3>
+      <button type="button" class="menu-button button-more has-dropdown js-dropdown-button default-icon" aria-haspopup="true" aria-expanded="false" aria-controls="${_("Usermenu")}">
+          <span class="icon-fallback icon-fallback-img">
+            <span class="icon icon-angle-down" aria-hidden="true"></span>
+            <span class="sr-only">${_("Usermenu dropdown")}</span>
+          </span>
+      </button>
+      <ul class="dropdown-menu list-divided is-hidden" id="${_("Usermenu")}" tabindex="-1">
+          <%block name="navigation_dropdown_menu_links" >
+              <li class="dropdown-item item has-block-link">
+                <a href="/">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
+              </li>
+          </%block>
+          <li class="dropdown-item item has-block-link">
+            <a class="action action-signout" href="${reverse('logout')}">${_("Sign Out")}</a>
+          </li>
+      </ul>
+  </div>
+
+% else:
+  <h3 class="title">
+    <span class="label">
+      <span class="label-prefix sr-only">${_("Currently signed in as:")}</span>
+      <span class="account-username" title="${ user.username }">${ user.username }</span>
+    </span>
+    <span class="icon fa fa-caret-down ui-toggle-dd" aria-hidden="true"></span>
+  </h3>
+
+  <div class="wrapper wrapper-nav-sub">
+    <div class="nav-sub">
+      <ul>
+        <li class="nav-item nav-account-dashboard">
+          <a href="/">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
+        </li>
+        <li class="nav-item nav-account-signout">
+          <a class="action action-signout" href="${reverse('logout')}">${_("Sign Out")}</a>
+        </li>
+      </ul>
+    </div>
+  </div>
+% endif
\ No newline at end of file
diff --git a/common/test/acceptance/fixtures/programs.py b/common/test/acceptance/fixtures/programs.py
index b35f6b3561f..baaea091803 100644
--- a/common/test/acceptance/fixtures/programs.py
+++ b/common/test/acceptance/fixtures/programs.py
@@ -31,16 +31,13 @@ class ProgramsFixture(object):
 
 class ProgramsConfigMixin(object):
     """Mixin providing a method used to configure the programs feature."""
-    def set_programs_api_configuration(self, is_enabled=False, api_version=1, api_url=PROGRAMS_STUB_URL,
-                                       js_path='/js', css_path='/css'):
+    def set_programs_api_configuration(self, is_enabled=False, api_version=1, api_url=PROGRAMS_STUB_URL):
         """Dynamically adjusts the Programs config model during tests."""
         ConfigModelFixture('/config/programs', {
             'enabled': is_enabled,
             'api_version_number': api_version,
             'internal_service_url': api_url,
             'public_service_url': api_url,
-            'authoring_app_js_path': js_path,
-            'authoring_app_css_path': css_path,
             'cache_ttl': 0,
             'enable_student_dashboard': is_enabled,
             'enable_studio_tab': is_enabled,
diff --git a/openedx/core/djangoapps/programs/models.py b/openedx/core/djangoapps/programs/models.py
index affc4cdeee9..3f54d5694f5 100644
--- a/openedx/core/djangoapps/programs/models.py
+++ b/openedx/core/djangoapps/programs/models.py
@@ -1,5 +1,4 @@
 """Models providing Programs support for the LMS and Studio."""
-from collections import namedtuple
 from urlparse import urljoin
 
 from django.utils.translation import ugettext_lazy as _
@@ -8,9 +7,6 @@ from django.db import models
 from config_models.models import ConfigurationModel
 
 
-AuthoringAppConfig = namedtuple('AuthoringAppConfig', ['js_url', 'css_url'])
-
-
 class ProgramsApiConfig(ConfigurationModel):
     """
     Manages configuration for connecting to the Programs service and using its
@@ -25,6 +21,7 @@ class ProgramsApiConfig(ConfigurationModel):
     internal_service_url = models.URLField(verbose_name=_("Internal Service URL"))
     public_service_url = models.URLField(verbose_name=_("Public Service URL"))
 
+    # TODO: The property below is obsolete. Delete at the earliest safe moment. See ECOM-4995
     authoring_app_js_path = models.CharField(
         verbose_name=_("Path to authoring app's JS"),
         max_length=255,
@@ -33,6 +30,8 @@ class ProgramsApiConfig(ConfigurationModel):
             "This value is required in order to enable the Studio authoring interface."
         )
     )
+
+    # TODO: The property below is obsolete. Delete at the earliest safe moment. See ECOM-4995
     authoring_app_css_path = models.CharField(
         verbose_name=_("Path to authoring app's CSS"),
         max_length=255,
@@ -103,17 +102,6 @@ class ProgramsApiConfig(ConfigurationModel):
         """
         return urljoin(self.public_service_url, '/api/v{}/'.format(self.api_version_number))
 
-    @property
-    def authoring_app_config(self):
-        """
-        Returns a named tuple containing information required for working with the Programs
-        authoring app, a Backbone app hosted by the Programs service.
-        """
-        js_url = urljoin(self.public_service_url, self.authoring_app_js_path)
-        css_url = urljoin(self.public_service_url, self.authoring_app_css_path)
-
-        return AuthoringAppConfig(js_url=js_url, css_url=css_url)
-
     @property
     def is_cache_enabled(self):
         """Whether responses from the Programs API will be cached."""
@@ -133,12 +121,7 @@ class ProgramsApiConfig(ConfigurationModel):
         Indicates whether Studio functionality related to Programs should
         be enabled or not.
         """
-        return (
-            self.enabled and
-            self.enable_studio_tab and
-            bool(self.authoring_app_js_path) and
-            bool(self.authoring_app_css_path)
-        )
+        return self.enabled and self.enable_studio_tab
 
     @property
     def is_certification_enabled(self):
diff --git a/openedx/core/djangoapps/programs/tests/mixins.py b/openedx/core/djangoapps/programs/tests/mixins.py
index 2a9c4360e88..3b6bf79d7f2 100644
--- a/openedx/core/djangoapps/programs/tests/mixins.py
+++ b/openedx/core/djangoapps/programs/tests/mixins.py
@@ -15,8 +15,6 @@ class ProgramsApiConfigMixin(object):
         'api_version_number': 1,
         'internal_service_url': 'http://internal.programs.org/',
         'public_service_url': 'http://public.programs.org/',
-        'authoring_app_js_path': '/path/to/js',
-        'authoring_app_css_path': '/path/to/css',
         'cache_ttl': 0,
         'enable_student_dashboard': True,
         'enable_studio_tab': True,
diff --git a/openedx/core/djangoapps/programs/tests/test_models.py b/openedx/core/djangoapps/programs/tests/test_models.py
index 8b2118e6627..71386950c76 100644
--- a/openedx/core/djangoapps/programs/tests/test_models.py
+++ b/openedx/core/djangoapps/programs/tests/test_models.py
@@ -26,17 +26,6 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
             programs_config.public_service_url.strip('/') + '/api/v{}/'.format(programs_config.api_version_number)
         )
 
-        authoring_app_config = programs_config.authoring_app_config
-
-        self.assertEqual(
-            authoring_app_config.js_url,
-            programs_config.public_service_url.strip('/') + programs_config.authoring_app_js_path
-        )
-        self.assertEqual(
-            authoring_app_config.css_url,
-            programs_config.public_service_url.strip('/') + programs_config.authoring_app_css_path
-        )
-
     @ddt.data(
         (0, False),
         (1, True),
@@ -72,9 +61,6 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
         programs_config = self.create_programs_config(enable_studio_tab=False)
         self.assertFalse(programs_config.is_studio_tab_enabled)
 
-        programs_config = self.create_programs_config(authoring_app_js_path='', authoring_app_css_path='')
-        self.assertFalse(programs_config.is_studio_tab_enabled)
-
         programs_config = self.create_programs_config()
         self.assertTrue(programs_config.is_studio_tab_enabled)
 
diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py
index 48b51dfdcbf..cb8d6676abf 100644
--- a/openedx/core/djangoapps/programs/utils.py
+++ b/openedx/core/djangoapps/programs/utils.py
@@ -20,7 +20,6 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data
 from student.models import CourseEnrollment
 from util.date_utils import strftime_localized
 from util.organizations_helpers import get_organization_by_short_name
-from xmodule.course_metadata_utils import DEFAULT_START_DATE
 
 
 log = logging.getLogger(__name__)
diff --git a/package.json b/package.json
index b8a766e9d6e..3ab10347425 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,9 @@
   "version": "0.1.0",
   "dependencies": {
     "backbone": "~1.3.2",
+    "backbone-validation": "~0.11.5",
     "coffee-script": "1.6.1",
-    "edx-pattern-library": "0.16.0",
+    "edx-pattern-library": "0.16.1",
     "edx-ui-toolkit": "1.4.1",
     "jquery": "~2.2.0",
     "jquery-migrate": "^1.4.1",
diff --git a/pavelib/assets.py b/pavelib/assets.py
index 269b6b0a9a4..c1320eb698a 100644
--- a/pavelib/assets.py
+++ b/pavelib/assets.py
@@ -53,6 +53,7 @@ NPM_INSTALLED_LIBRARIES = [
     'picturefill/dist/picturefill.js',
     'backbone/backbone.js',
     'edx-ui-toolkit/node_modules/backbone.paginator/lib/backbone.paginator.js',
+    'backbone-validation/dist/backbone-validation-min.js',
 ]
 
 # Directory to install static vendor files
diff --git a/themes/edx.org/cms/templates/widgets/sock.html b/themes/edx.org/cms/templates/widgets/sock.html
index 44f059a569d..571d90320c9 100644
--- a/themes/edx.org/cms/templates/widgets/sock.html
+++ b/themes/edx.org/cms/templates/widgets/sock.html
@@ -1,8 +1,8 @@
+<%page expression_filter="h" args="online_help_token"/>
 <%!
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
 %>
-<%page args="online_help_token"/>
 <div class="wrapper-sock wrapper">
   <ul class="list-actions list-cta">
     <li class="action-item">
@@ -16,7 +16,7 @@ from django.core.urlresolvers import reverse
   <div class="wrapper-inner wrapper">
     <section class="sock" id="sock">
       <header>
-        <h2 class="title sr">${_("{studio_name} Documentation").format(studio_name=settings.STUDIO_NAME)}</h2>
+        <h2 class="title sr-only">${_("{studio_name} Documentation").format(studio_name=settings.STUDIO_NAME)}</h2>
       </header>
 
       <div class="support">
-- 
GitLab