Commit 989a9796 by Carson Gee

Merge pull request #718 from edx/feature/ichuang/push-to-lms

Let a Studio user export course to git (and via git, to elsewhere, eg LMS)
parents 0c74d6ac 5e0f7816
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
CMS: Add feature to allow exporting a course to a git repository by
specifying the giturl in the course settings.
Studo: Fix import/export bug with conditional modules. STUD-149 Studo: Fix import/export bug with conditional modules. STUD-149
Blades: Persist student progress in video. BLD-385. Blades: Persist student progress in video. BLD-385.
......
"""
Utilities for export a course's XML into a git repository,
committing and pushing the changes.
"""
import logging
import os
import subprocess
from urlparse import urlparse
from django.conf import settings
from django.contrib.auth.models import User
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_exporter import export_to_xml
log = logging.getLogger(__name__)
GIT_REPO_EXPORT_DIR = getattr(settings, 'GIT_REPO_EXPORT_DIR', None)
GIT_EXPORT_DEFAULT_IDENT = getattr(settings, 'GIT_EXPORT_DEFAULT_IDENT',
{'name': 'STUDIO_EXPORT_TO_GIT',
'email': 'STUDIO_EXPORT_TO_GIT@example.com'})
class GitExportError(Exception):
"""
Convenience exception class for git export error conditions.
"""
NO_EXPORT_DIR = _("GIT_REPO_EXPORT_DIR not set or path {0} doesn't exist, "
"please create it, or configure a different path with "
"GIT_REPO_EXPORT_DIR".format(GIT_REPO_EXPORT_DIR))
URL_BAD = _('Non writable git url provided. Expecting something like:'
' git@github.com:mitocw/edx4edx_lite.git')
URL_NO_AUTH = _('If using http urls, you must provide the username '
'and password in the url. Similar to '
'https://user:pass@github.com/user/course.')
DETACHED_HEAD = _('Unable to determine branch, repo in detached HEAD mode')
CANNOT_PULL = _('Unable to update or clone git repository.')
XML_EXPORT_FAIL = _('Unable to export course to xml.')
CONFIG_ERROR = _('Unable to configure git username and password')
CANNOT_COMMIT = _('Unable to commit changes. This is usually '
'because there are no changes to be committed')
CANNOT_PUSH = _('Unable to push changes. This is usually '
'because the remote repository cannot be contacted')
BAD_COURSE = _('Bad course location provided')
MISSING_BRANCH = _('Missing branch on fresh clone')
def cmd_log(cmd, cwd):
"""
Helper function to redirect stderr to stdout and log the command
used along with the output. Will raise subprocess.CalledProcessError if
command doesn't return 0, and returns the command's output.
"""
output = subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT)
log.debug(_('Command was: {0!r}. '
'Working directory was: {1!r}'.format(' '.join(cmd), cwd)))
log.debug(_('Command output was: {0!r}'.format(output)))
return output
def export_to_git(course_loc, repo, user='', rdir=None):
"""Export a course to git."""
# pylint: disable=R0915
if course_loc.startswith('i4x://'):
course_loc = course_loc[6:]
if not GIT_REPO_EXPORT_DIR:
raise GitExportError(GitExportError.NO_EXPORT_DIR)
if not os.path.isdir(GIT_REPO_EXPORT_DIR):
raise GitExportError(GitExportError.NO_EXPORT_DIR)
# Check for valid writable git url
if not (repo.endswith('.git') or
repo.startswith(('http:', 'https:', 'file:'))):
raise GitExportError(GitExportError.URL_BAD)
# Check for username and password if using http[s]
if repo.startswith('http:') or repo.startswith('https:'):
parsed = urlparse(repo)
if parsed.username is None or parsed.password is None:
raise GitExportError(GitExportError.URL_NO_AUTH)
if rdir:
rdir = os.path.basename(rdir)
else:
rdir = repo.rsplit('/', 1)[-1].rsplit('.git', 1)[0]
log.debug("rdir = %s", rdir)
# Pull or clone repo before exporting to xml
# and update url in case origin changed.
rdirp = '{0}/{1}'.format(GIT_REPO_EXPORT_DIR, rdir)
branch = None
if os.path.exists(rdirp):
log.info(_('Directory already exists, doing a git reset and pull '
'instead of git clone.'))
cwd = rdirp
# Get current branch
cmd = ['git', 'symbolic-ref', '--short', 'HEAD']
try:
branch = cmd_log(cmd, cwd).strip('\n')
except subprocess.CalledProcessError as ex:
log.exception('Failed to get branch: %r', ex.output)
raise GitExportError(GitExportError.DETACHED_HEAD)
cmds = [
['git', 'remote', 'set-url', 'origin', repo],
['git', 'fetch', 'origin'],
['git', 'reset', '--hard', 'origin/{0}'.format(branch)],
['git', 'pull'],
]
else:
cmds = [['git', 'clone', repo]]
cwd = GIT_REPO_EXPORT_DIR
cwd = os.path.abspath(cwd)
for cmd in cmds:
try:
cmd_log(cmd, cwd)
except subprocess.CalledProcessError as ex:
log.exception('Failed to pull git repository: %r', ex.output)
raise GitExportError(GitExportError.CANNOT_PULL)
# export course as xml before commiting and pushing
try:
location = CourseDescriptor.id_to_location(course_loc)
except ValueError:
raise GitExportError(GitExportError.BAD_COURSE)
root_dir = os.path.dirname(rdirp)
course_dir = os.path.splitext(os.path.basename(rdirp))[0]
try:
export_to_xml(modulestore('direct'), contentstore(), location,
root_dir, course_dir, modulestore())
except (EnvironmentError, AttributeError):
log.exception('Failed export to xml')
raise GitExportError(GitExportError.XML_EXPORT_FAIL)
# Get current branch if not already set
if not branch:
cmd = ['git', 'symbolic-ref', '--short', 'HEAD']
try:
branch = cmd_log(cmd, os.path.abspath(rdirp)).strip('\n')
except subprocess.CalledProcessError as ex:
log.exception('Failed to get branch from freshly cloned repo: %r',
ex.output)
raise GitExportError(GitExportError.MISSING_BRANCH)
# Now that we have fresh xml exported, set identity, add
# everything to git, commit, and push to the right branch.
ident = {}
try:
user = User.objects.get(username=user)
ident['name'] = user.username
ident['email'] = user.email
except User.DoesNotExist:
# That's ok, just use default ident
ident = GIT_EXPORT_DEFAULT_IDENT
time_stamp = timezone.now()
cwd = os.path.abspath(rdirp)
commit_msg = 'Export from Studio at {1}'.format(user, time_stamp)
try:
cmd_log(['git', 'config', 'user.email', ident['email']], cwd)
cmd_log(['git', 'config', 'user.name', ident['name']], cwd)
except subprocess.CalledProcessError as ex:
log.exception('Error running git configure commands: %r', ex.output)
raise GitExportError(GitExportError.CONFIG_ERROR)
try:
cmd_log(['git', 'add', '.'], cwd)
cmd_log(['git', 'commit', '-a', '-m', commit_msg], cwd)
except subprocess.CalledProcessError as ex:
log.exception('Unable to commit changes: %r', ex.output)
raise GitExportError(GitExportError.CANNOT_COMMIT)
try:
cmd_log(['git', 'push', '-q', 'origin', branch], cwd)
except subprocess.CalledProcessError as ex:
log.exception('Error running git push command: %r', ex.output)
raise GitExportError(GitExportError.CANNOT_PUSH)
"""
This command exports a course from CMS to a git repository.
It takes as arguments the course id to export (i.e MITx/999/2020 ) and
the repository to commit too. It takes username as an option for identifying
the commit, as well as a directory path to place the git repository.
By default it will use settings.GIT_REPO_EXPORT_DIR/repo_name as the cloned
directory. It is branch aware, but will reset all local changes to the
repository before attempting to export the XML, add, and commit changes if
any have taken place.
This functionality is also available as an export view in studio if the giturl
attribute is set and the FEATURE['ENABLE_EXPORT_GIT'] is set.
"""
import logging
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.utils.translation import ugettext as _
import contentstore.git_export_utils as git_export_utils
log = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Take a course from studio and export it to a git repository.
"""
option_list = BaseCommand.option_list + (
make_option('--username', '-u', dest='user',
help=('Specify a username from LMS/Studio to be used '
'as the commit author.')),
make_option('--repo_dir', '-r', dest='repo',
help='Specify existing git repo directory.'),
)
help = _('Take the specified course and attempt to '
'export it to a git repository\n. Course directory '
'must already be a git repository. Usage: '
' git_export <course_loc> <git_url>')
def handle(self, *args, **options):
"""
Checks arguments and runs export function if they are good
"""
if len(args) != 2:
raise CommandError('This script requires exactly two arguments: '
'course_loc and git_url')
# Rethrow GitExportError as CommandError for SystemExit
try:
git_export_utils.export_to_git(
args[0],
args[1],
options.get('user', ''),
options.get('rdir', None)
)
except git_export_utils.GitExportError as ex:
raise CommandError(str(ex))
"""
Unittests for exporting to git via management command.
"""
import copy
import os
import shutil
import StringIO
import subprocess
import unittest
from uuid import uuid4
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test.utils import override_settings
from contentstore.tests.utils import CourseTestCase
import contentstore.git_export_utils as git_export_utils
from contentstore.git_export_utils import GitExportError
FEATURES_WITH_EXPORT_GIT = settings.FEATURES.copy()
FEATURES_WITH_EXPORT_GIT['ENABLE_EXPORT_GIT'] = True
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
@override_settings(FEATURES=FEATURES_WITH_EXPORT_GIT)
class TestGitExport(CourseTestCase):
"""
Excercise the git_export django management command with various inputs.
"""
def setUp(self):
"""
Create/reinitialize bare repo and folders needed
"""
super(TestGitExport, self).setUp()
if not os.path.isdir(git_export_utils.GIT_REPO_EXPORT_DIR):
os.mkdir(git_export_utils.GIT_REPO_EXPORT_DIR)
self.addCleanup(shutil.rmtree, git_export_utils.GIT_REPO_EXPORT_DIR)
self.bare_repo_dir = '{0}/data/test_bare.git'.format(
os.path.abspath(settings.TEST_ROOT))
if not os.path.isdir(self.bare_repo_dir):
os.mkdir(self.bare_repo_dir)
self.addCleanup(shutil.rmtree, self.bare_repo_dir)
subprocess.check_output(['git', '--bare', 'init'],
cwd=self.bare_repo_dir)
def test_command(self):
"""
Test that the command interface works. Ignore stderr fo clean
test output.
"""
with self.assertRaises(SystemExit) as ex:
with self.assertRaisesRegexp(CommandError, 'This script requires.*'):
call_command('git_export', 'blah', 'blah', 'blah',
stderr=StringIO.StringIO())
self.assertEqual(ex.exception.code, 1)
with self.assertRaises(SystemExit) as ex:
with self.assertRaisesRegexp(CommandError, 'This script requires.*'):
call_command('git_export', stderr=StringIO.StringIO())
self.assertEqual(ex.exception.code, 1)
# Send bad url to get course not exported
with self.assertRaises(SystemExit) as ex:
with self.assertRaisesRegexp(CommandError, GitExportError.URL_BAD):
call_command('git_export', 'foo', 'silly',
stderr=StringIO.StringIO())
self.assertEqual(ex.exception.code, 1)
def test_bad_git_url(self):
"""
Test several bad URLs for validation
"""
with self.assertRaisesRegexp(GitExportError, str(GitExportError.URL_BAD)):
git_export_utils.export_to_git('', 'Sillyness')
with self.assertRaisesRegexp(GitExportError, str(GitExportError.URL_BAD)):
git_export_utils.export_to_git('', 'example.com:edx/notreal')
with self.assertRaisesRegexp(GitExportError,
str(GitExportError.URL_NO_AUTH)):
git_export_utils.export_to_git('', 'http://blah')
def test_bad_git_repos(self):
"""
Test invalid git repos
"""
test_repo_path = '{}/test_repo'.format(git_export_utils.GIT_REPO_EXPORT_DIR)
self.assertFalse(os.path.isdir(test_repo_path))
# Test bad clones
with self.assertRaisesRegexp(GitExportError,
str(GitExportError.CANNOT_PULL)):
git_export_utils.export_to_git(
'foo/blah/100',
'https://user:blah@example.com/test_repo.git')
self.assertFalse(os.path.isdir(test_repo_path))
# Setup good repo with bad course to test xml export
with self.assertRaisesRegexp(GitExportError,
str(GitExportError.XML_EXPORT_FAIL)):
git_export_utils.export_to_git(
'foo/blah/100',
'file://{0}'.format(self.bare_repo_dir))
# Test bad git remote after successful clone
with self.assertRaisesRegexp(GitExportError,
str(GitExportError.CANNOT_PULL)):
git_export_utils.export_to_git(
'foo/blah/100',
'https://user:blah@example.com/r.git')
def test_bad_course_id(self):
"""
Test valid git url, but bad course.
"""
with self.assertRaisesRegexp(GitExportError, str(GitExportError.BAD_COURSE)):
git_export_utils.export_to_git(
'', 'file://{0}'.format(self.bare_repo_dir), '', '/blah')
@unittest.skipIf(os.environ.get('GIT_CONFIG') or
os.environ.get('GIT_AUTHOR_EMAIL') or
os.environ.get('GIT_AUTHOR_NAME') or
os.environ.get('GIT_COMMITTER_EMAIL') or
os.environ.get('GIT_COMMITTER_NAME'),
'Global git override set')
def test_git_ident(self):
"""
Test valid course with and without user specified.
Test skipped if git global config override environment variable GIT_CONFIG
is set.
"""
git_export_utils.export_to_git(
self.course.id,
'file://{0}'.format(self.bare_repo_dir),
'enigma'
)
expect_string = '{0}|{1}\n'.format(
git_export_utils.GIT_EXPORT_DEFAULT_IDENT['name'],
git_export_utils.GIT_EXPORT_DEFAULT_IDENT['email']
)
cwd = os.path.abspath(git_export_utils.GIT_REPO_EXPORT_DIR / 'test_bare')
git_log = subprocess.check_output(['git', 'log', '-1',
'--format=%an|%ae'], cwd=cwd)
self.assertEqual(expect_string, git_log)
# Make changes to course so there is something commit
self.populateCourse()
git_export_utils.export_to_git(
self.course.id,
'file://{0}'.format(self.bare_repo_dir),
self.user.username
)
expect_string = '{0}|{1}\n'.format(
self.user.username,
self.user.email,
)
git_log = subprocess.check_output(
['git', 'log', '-1', '--format=%an|%ae'], cwd=cwd)
self.assertEqual(expect_string, git_log)
def test_no_change(self):
"""
Test response if there are no changes
"""
git_export_utils.export_to_git(
'i4x://{0}'.format(self.course.id),
'file://{0}'.format(self.bare_repo_dir)
)
with self.assertRaisesRegexp(GitExportError,
str(GitExportError.CANNOT_COMMIT)):
git_export_utils.export_to_git(
self.course.id, 'file://{0}'.format(self.bare_repo_dir))
"""
Test the ability to export courses to xml from studio
"""
import copy
import os
import shutil
import subprocess
from uuid import uuid4
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from pymongo import MongoClient
from .utils import CourseTestCase
import contentstore.git_export_utils as git_export_utils
from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore.django import modulestore
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class TestExportGit(CourseTestCase):
"""
Tests pushing a course to a git repository
"""
def setUp(self):
"""
Setup test course, user, and url.
"""
super(TestExportGit, self).setUp()
self.course_module = modulestore().get_item(self.course.location)
self.test_url = reverse('export_git', kwargs={
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
def tearDown(self):
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'])
_CONTENTSTORE.clear()
def test_giturl_missing(self):
"""
Test to make sure an appropriate error is displayed
if course hasn't set giturl.
"""
response = self.client.get(self.test_url)
self.assertEqual(200, response.status_code)
self.assertIn(
('giturl must be defined in your '
'course settings before you can export to git.'),
response.content
)
response = self.client.get('{}?action=push'.format(self.test_url))
self.assertEqual(200, response.status_code)
self.assertIn(
('giturl must be defined in your '
'course settings before you can export to git.'),
response.content
)
def test_course_export_failures(self):
"""
Test failed course export response.
"""
self.course_module.giturl = 'foobar'
modulestore().save_xmodule(self.course_module)
response = self.client.get('{}?action=push'.format(self.test_url))
self.assertIn('Export Failed:', response.content)
def test_course_export_success(self):
"""
Test successful course export response.
"""
# Build out local bare repo, and set course git url to it
repo_dir = os.path.abspath(git_export_utils.GIT_REPO_EXPORT_DIR)
os.mkdir(repo_dir)
self.addCleanup(shutil.rmtree, repo_dir)
bare_repo_dir = '{0}/test_repo.git'.format(
os.path.abspath(git_export_utils.GIT_REPO_EXPORT_DIR))
os.mkdir(bare_repo_dir)
self.addCleanup(shutil.rmtree, bare_repo_dir)
subprocess.check_output(['git', '--bare', 'init', ], cwd=bare_repo_dir)
self.populateCourse()
self.course_module.giturl = 'file://{}'.format(bare_repo_dir)
modulestore().save_xmodule(self.course_module)
response = self.client.get('{}?action=push'.format(self.test_url))
self.assertIn('Export Succeeded', response.content)
...@@ -14,6 +14,7 @@ from .item import * ...@@ -14,6 +14,7 @@ from .item import *
from .import_export import * from .import_export import *
from .preview import * from .preview import *
from .public import * from .public import *
from .export_git import *
from .user import * from .user import *
from .tabs import * from .tabs import *
from .transcripts_ajax import * from .transcripts_ajax import *
......
"""
This views handles exporting the course xml to a git repository if
the giturl attribute is set.
"""
import logging
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from django.utils.translation import ugettext as _
from .access import has_course_access
import contentstore.git_export_utils as git_export_utils
from edxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__)
@ensure_csrf_cookie
@login_required
def export_git(request, org, course, name):
"""
This method serves up the 'Export to Git' page
"""
location = Location('i4x', org, course, 'course', name)
if not has_course_access(request.user, location):
raise PermissionDenied()
course_module = modulestore().get_item(location)
failed = False
log.debug('export_git course_module=%s', course_module)
msg = ""
if 'action' in request.GET and course_module.giturl:
if request.GET['action'] == 'push':
try:
git_export_utils.export_to_git(
course_module.id,
course_module.giturl,
request.user,
)
msg = _('Course successfully exported to git repository')
except git_export_utils.GitExportError as ex:
failed = True
msg = str(ex)
return render_to_response('export_git.html', {
'context_course': course_module,
'msg': msg,
'failed': failed,
})
...@@ -156,6 +156,9 @@ THEME_NAME = ENV_TOKENS.get('THEME_NAME', None) ...@@ -156,6 +156,9 @@ THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
# Push to LMS overrides
GIT_REPO_EXPORT_DIR = ENV_TOKENS.get('GIT_REPO_EXPORT_DIR', '/edx/var/edxapp/export_course_repos')
# Translation overrides # Translation overrides
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
......
...@@ -37,6 +37,10 @@ STATIC_ROOT = TEST_ROOT / "staticfiles" ...@@ -37,6 +37,10 @@ STATIC_ROOT = TEST_ROOT / "staticfiles"
GITHUB_REPO_ROOT = TEST_ROOT / "data" GITHUB_REPO_ROOT = TEST_ROOT / "data"
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
# For testing "push to lms"
FEATURES['ENABLE_EXPORT_GIT'] = True
GIT_REPO_EXPORT_DIR = TEST_ROOT / "export_course_repos"
# Makes the tests run much faster... # Makes the tests run much faster...
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
@import 'views/users'; @import 'views/users';
@import 'views/checklists'; @import 'views/checklists';
@import 'views/textbooks'; @import 'views/textbooks';
@import 'views/export-git';
// base - contexts // base - contexts
@import 'contexts/ie'; // ie-specific rules (mostly for known/older bugs) @import 'contexts/ie'; // ie-specific rules (mostly for known/older bugs)
......
// studio - views - export to git
// ====================
.view-export-git {
// UI: basic layout
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(9,12);
margin-right: flex-gutter();
}
.content-supplementary {
width: flex-grid(3,12);
}
.error-text {
color: $error-red;
}
h3 {
font-size: 19px;
font-weight: 700;
}
.export-git-info-block {
dt {
font-size: 19px;
font-weight: 700;
margin-top: 12px;
}
dd {
font-size: 17px;
margin-bottom: 20px;
}
.course_text {
color: $green;
}
.giturl_text {
color: $blue;
}
}
// UI: introduction
.introduction {
.title {
@extend %cont-text-sr;
}
}
// UI: export controls
.export-git-controls {
@include box-sizing(border-box);
@extend %ui-window;
padding: $baseline ($baseline*1.5) ($baseline*1.5) ($baseline*1.5);
.title {
@extend %t-title4;
}
.action-export-git {
@extend %btn-primary-blue;
@extend %t-action1;
display: block;
margin: $baseline 0;
padding: ($baseline*0.75) $baseline;
}
.action {
[class^="icon"] {
@extend %t-icon2;
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
.copy {
display: inline-block;
vertical-align: middle;
}
}
}
}
<%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
%>
<%block name="title">${_("Export Course to Git")}</%block>
<%block name="bodyclass">is-signedin course tools view-export-git</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>${_("Export to Git")}
</h1>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<div class="introduction">
<h2 class="title">${_("About Export to Git")}</h2>
<div class="copy">
<p>${_("Use this to export your course to its git repository.")}</p>
<p>${_("This will then trigger an automatic update of the main LMS site and update the contents of your course visible there to students if automatic git imports are configured.")}</p>
</div>
</div>
<div class="export-git-controls">
<h2 class="title">${_("Export Course to Git:")}</h2>
% if not context_course.giturl:
<p class="error-text">${_("giturl must be defined in your course settings before you can export to git.")}</p>
% else:
<ul class="list-actions">
<li class="item-action">
<a class="action action-export-git"" action-primary" href="${reverse('export_git', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}?action=push">
<i class="icon-download"></i>
<span class="copy">${_("Export to Git")}</span>
</a>
</li>
</ul>
% endif
</div>
<div class="messages">
% if msg:
% if failed:
<h3 class="error-text">${_('Export Failed')}:</h3>
% else:
<h3>${_('Export Succeeded')}:</h3>
% endif
<pre>${msg|h}</pre>
% endif
</div>
</article>
<aside class="content-supplementary" role="complimentary">
<dl class="export-git-info-block">
<dt>${_("Your course:")}</dt>
<dd class="course_text">${context_course.id}</dd>
<dt>${_("Course git url:")}</dt>
<dd class="giturl_text">${context_course.giturl}</dd>
</dl>
</aside>
</section>
</div>
</%block>
...@@ -104,6 +104,11 @@ ...@@ -104,6 +104,11 @@
<li class="nav-item nav-course-tools-export"> <li class="nav-item nav-course-tools-export">
<a href="${export_url}">${_("Export")}</a> <a href="${export_url}">${_("Export")}</a>
</li> </li>
% if settings.FEATURES.get('ENABLE_EXPORT_GIT') and context_course.giturl:
<li class="nav-item nav-course-tools-export-git">
<a href="${reverse('export_git', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Export to Git")}</a>
</li>
% endif
</ul> </ul>
</div> </div>
</div> </div>
......
...@@ -96,6 +96,11 @@ urlpatterns += patterns('', ...@@ -96,6 +96,11 @@ urlpatterns += patterns('',
url(r'^i18n.js$', 'django.views.i18n.javascript_catalog', js_info_dict), url(r'^i18n.js$', 'django.views.i18n.javascript_catalog', js_info_dict),
) )
if settings.FEATURES.get('ENABLE_EXPORT_GIT'):
urlpatterns += (url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/export_git/(?P<name>[^/]+)$',
'contentstore.views.export_git', name='export_git'),)
if settings.FEATURES.get('ENABLE_SERVICE_STATUS'): if settings.FEATURES.get('ENABLE_SERVICE_STATUS'):
urlpatterns += patterns('', urlpatterns += patterns('',
url(r'^status/', include('service_status.urls')), url(r'^status/', include('service_status.urls')),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment