Commit b42e750e by Calen Pennington

Merge pull request #425 from MITx/feature/bridger/new_wiki

Feature/bridger/new wiki
parents 6db5893b d81fb00f
[submodule "askbot"]
path = askbot
url = git@github.com:MITx/askbot-devel.git
[submodule "lms/djangoapps/django-wiki"]
path = lms/djangoapps/django-wiki
url = git@github.com:benjaoming/django-wiki.git
from django.template.base import TemplateDoesNotExist
from django.template.loader import make_origin, get_template_from_string
from django.template.loaders.filesystem import Loader as FilesystemLoader
from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
from mitxmako.template import Template
class MakoLoader(object):
"""
This is a Django loader object which will load the template as a
Mako template if the first line is "## mako". It is based off BaseLoader
in django.template.loader.
"""
is_usable = False
def __init__(self, base_loader):
# base_loader is an instance of a BaseLoader subclass
self.base_loader = base_loader
def __call__(self, template_name, template_dirs=None):
return self.load_template(template_name, template_dirs)
def load_template(self, template_name, template_dirs=None):
source, display_name = self.load_template_source(template_name, template_dirs)
if source.startswith("## mako\n"):
# This is a mako template
template = Template(text=source, uri=template_name)
return template, None
else:
# This is a regular template
origin = make_origin(display_name, self.load_template_source, template_name, template_dirs)
try:
template = get_template_from_string(source, origin, template_name)
return template, None
except TemplateDoesNotExist:
# If compiling the template we found raises TemplateDoesNotExist, back off to
# returning the source and display name for the template we were asked to load.
# This allows for correct identification (later) of the actual template that does
# not exist.
return source, display_name
def load_template_source(self, template_name, template_dirs=None):
# Just having this makes the template load as an instance, instead of a class.
return self.base_loader.load_template_source(template_name, template_dirs)
def reset(self):
self.base_loader.reset()
class MakoFilesystemLoader(MakoLoader):
is_usable = True
def __init__(self):
MakoLoader.__init__(self, FilesystemLoader())
class MakoAppDirectoriesLoader(MakoLoader):
is_usable = True
def __init__(self):
MakoLoader.__init__(self, AppDirectoriesLoader())
......@@ -12,18 +12,48 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.conf import settings
from mako.template import Template as MakoTemplate
from . import middleware
from mitxmako import middleware
django_variables = ['lookup', 'template_dirs', 'output_encoding',
django_variables = ['lookup', 'output_encoding',
'module_directory', 'encoding_errors']
# TODO: We should make this a Django Template subclass that simply has the MakoTemplate inside of it? (Intead of inheriting from MakoTemplate)
class Template(MakoTemplate):
"""
This bridges the gap between a Mako template and a djano template. It can
be rendered like it is a django template because the arguments are transformed
in a way that MakoTemplate can understand.
"""
def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False):
overrides = dict([(k, getattr(middleware, k, None),) for k in django_variables])
overrides['lookup'] = overrides['lookup']['main']
kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs)
def render(self, context_instance):
"""
This takes a render call with a context (from Django) and translates
it to a render call on the mako template.
"""
# collapse context_instance to a single dictionary for mako
context_dictionary = {}
# In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None:
for d in middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
context_dictionary['settings'] = settings
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
context_dictionary['django_context'] = context_instance
return super(Template, self).render(**context_dictionary)
from django.template import loader
from django.template.base import Template, Context
from django.template.loader import get_template, select_template
def django_template_include(file_name, mako_context):
"""
This can be used within a mako template to include a django template
in the way that a django-style {% include %} does. Pass it context
which can be the mako context ('context') or a dictionary.
"""
dictionary = dict( mako_context )
return loader.render_to_string(file_name, dictionary=dictionary)
def render_inclusion(func, file_name, takes_context, django_context, *args, **kwargs):
"""
This allows a mako template to call a template tag function (written
for django templates) that is an "inclusion tag". These functions are
decorated with @register.inclusion_tag.
-func: This is the function that is registered as an inclusion tag.
You must import it directly using a python import statement.
-file_name: This is the filename of the template, passed into the
@register.inclusion_tag statement.
-takes_context: This is a parameter of the @register.inclusion_tag.
-django_context: This is an instance of the django context. If this
is a mako template rendered through the regular django rendering calls,
a copy of the django context is available as 'django_context'.
-*args and **kwargs are the arguments to func.
"""
if takes_context:
args = [django_context] + list(args)
_dict = func(*args, **kwargs)
if isinstance(file_name, Template):
t = file_name
elif not isinstance(file_name, basestring) and is_iterable(file_name):
t = select_template(file_name)
else:
t = get_template(file_name)
nodelist = t.nodelist
new_context = Context(_dict)
csrf_token = django_context.get('csrf_token', None)
if csrf_token is not None:
new_context['csrf_token'] = csrf_token
return nodelist.render(new_context)
......@@ -147,7 +147,7 @@ class CourseDescriptor(SequenceDescriptor):
return self.location.course
@property
def wiki_namespace(self):
def wiki_slug(self):
return self.location.course
@property
......
import re
from urlparse import urlparse
from django.http import Http404
from django.shortcuts import redirect
from wiki.models import reverse as wiki_reverse
from courseware.courses import get_course_with_access
IN_COURSE_WIKI_REGEX = r'/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/wiki/(?P<wiki_path>.*|)$'
class Middleware(object):
"""
This middleware is to keep the course nav bar above the wiki while
the student clicks around to other wiki pages.
If it intercepts a request for /wiki/.. that has a referrer in the
form /courses/course_id/... it will redirect the user to the page
/courses/course_id/wiki/...
It is also possible that someone followed a link leading to a course
that they don't have access to. In this case, we redirect them to the
same page on the regular wiki.
If we return a redirect, this middleware makes sure that the redirect
keeps the student in the course.
Finally, if the student is in the course viewing a wiki, we change the
reverse() function to resolve wiki urls as a course wiki url by setting
the _transform_url attribute on wiki.models.reverse.
Forgive me Father, for I have hacked.
"""
def __init__(self):
self.redirected = False
def process_request(self, request):
self.redirected = False
wiki_reverse._transform_url = lambda url: url
referer = request.META.get('HTTP_REFERER')
destination = request.path
if request.method == 'GET':
new_destination = self.get_redirected_url(request.user, referer, destination)
if new_destination != destination:
# We mark that we generated this redirection, so we don't modify it again
self.redirected = True
return redirect(new_destination)
course_match = re.match(IN_COURSE_WIKI_REGEX, destination)
if course_match:
course_id = course_match.group('course_id')
prepend_string = '/courses/' + course_match.group('course_id')
wiki_reverse._transform_url = lambda url: prepend_string + url
return None
def process_response(self, request, response):
"""
If this is a redirect response going to /wiki/*, then we might need
to change it to be a redirect going to /courses/*/wiki*.
"""
if not self.redirected and response.status_code == 302: #This is a redirect
referer = request.META.get('HTTP_REFERER')
destination_url = response['LOCATION']
destination = urlparse(destination_url).path
new_destination = self.get_redirected_url(request.user, referer, destination)
if new_destination != destination:
new_url = destination_url.replace(destination, new_destination)
response['LOCATION'] = new_url
return response
def get_redirected_url(self, user, referer, destination):
"""
Returns None if the destination shouldn't be changed.
"""
if not referer:
return destination
referer_path = urlparse(referer).path
path_match = re.match(r'^/wiki/(?P<wiki_path>.*|)$', destination)
if path_match:
# We are going to the wiki. Check if we came from a course
course_match = re.match(r'/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/.*', referer_path)
if course_match:
course_id = course_match.group('course_id')
# See if we are able to view the course. If we are, redirect to it
try:
course = get_course_with_access(user, course_id, 'load')
return "/courses/" + course.id + "/wiki/" + path_match.group('wiki_path')
except Http404:
# Even though we came from the course, we can't see it. So don't worry about it.
pass
else:
# It is also possible we are going to a course wiki view, but we
# don't have permission to see the course!
course_match = re.match(IN_COURSE_WIKI_REGEX, destination)
if course_match:
course_id = course_match.group('course_id')
# See if we are able to view the course. If we aren't, redirect to regular wiki
try:
course = get_course_with_access(user, course_id, 'load')
# Good, we can see the course. Carry on
return destination
except Http404:
# We can't see the course, so redirect to the regular wiki
return "/wiki/" + course_match.group('wiki_path')
return destination
def context_processor(request):
"""
This is a context processor which looks at the URL while we are
in the wiki. If the url is in the form
/courses/(course_id)/wiki/...
then we add 'course' to the context. This allows the course nav
bar to be shown.
"""
match = re.match(IN_COURSE_WIKI_REGEX, request.path)
if match:
course_id = match.group('course_id')
try:
course = get_course_with_access(request.user, course_id, 'load')
return {'course' : course}
except Http404:
# We couldn't access the course for whatever reason. It is too late to change
# the URL here, so we just leave the course context. The middleware shouldn't
# let this happen
pass
return {}
\ No newline at end of file
from django.core.urlresolvers import reverse
from override_settings import override_settings
import xmodule.modulestore.django
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class WikiRedirectTestCase(PageLoader):
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
def find_course(name):
"""Assumes the course is present"""
return [c for c in courses if c.location.course==name][0]
self.full = find_course("full")
self.toy = find_course("toy")
# Create two accounts
self.student = 'view@test.com'
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u1', self.student, self.password)
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.student)
self.activate_user(self.instructor)
def test_wiki_redirect(self):
"""
Test that requesting wiki URLs redirect properly to or out of classes.
An enrolled in student going from /courses/edX/toy/2012_Fall/profile
to /wiki/some/fake/wiki/page/ will redirect to
/courses/edX/toy/2012_Fall/wiki/some/fake/wiki/page/
An unenrolled student going to /courses/edX/toy/2012_Fall/wiki/some/fake/wiki/page/
will be redirected to /wiki/some/fake/wiki/page/
"""
self.login(self.student, self.password)
self.enroll(self.toy)
referer = reverse("profile", kwargs={ 'course_id' : self.toy.id })
destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'})
redirected_to = referer.replace("profile", "wiki/some/fake/wiki/page/")
resp = self.client.get( destination, HTTP_REFERER=referer)
self.assertEqual(resp.status_code, 302 )
self.assertEqual(resp['Location'], 'http://testserver' + redirected_to )
# Now we test that the student will be redirected away from that page if the course doesn't exist
# We do this in the same test because we want to make sure the redirected_to is constructed correctly
# This is a location like /courses/*/wiki/* , but with an invalid course ID
bad_course_wiki_page = redirected_to.replace( self.toy.location.course, "bad_course" )
resp = self.client.get( bad_course_wiki_page, HTTP_REFERER=referer)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp['Location'], 'http://testserver' + destination )
def create_course_page(self, course):
"""
Test that loading the course wiki page creates the wiki page.
The user must be enrolled in the course to see the page.
"""
course_wiki_home = reverse('course_wiki', kwargs={'course_id' : course.id})
referer = reverse("profile", kwargs={ 'course_id' : self.toy.id })
resp = self.client.get(course_wiki_home, follow=True, HTTP_REFERER=referer)
course_wiki_page = referer.replace('profile', 'wiki/' + self.toy.wiki_slug + "/")
ending_location = resp.redirect_chain[-1][0]
ending_status = resp.redirect_chain[-1][1]
self.assertEquals(ending_location, 'http://testserver' + course_wiki_page )
self.assertEquals(resp.status_code, 200)
self.has_course_navigator(resp)
def has_course_navigator(self, resp):
"""
Ensure that the response has the course navigator.
"""
self.assertTrue( "course info" in resp.content.lower() )
self.assertTrue( "courseware" in resp.content.lower() )
def test_course_navigator(self):
""""
Test that going from a course page to a wiki page contains the course navigator.
"""
self.login(self.student, self.password)
self.enroll(self.toy)
self.create_course_page(self.toy)
course_wiki_page = reverse('wiki:get', kwargs={'path' : self.toy.wiki_slug + '/'})
referer = reverse("courseware", kwargs={ 'course_id' : self.toy.id })
resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer)
self.has_course_navigator(resp)
import logging
import re
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import redirect
from wiki.core.exceptions import NoRootURL
from wiki.models import URLPath, Article
from courseware.courses import get_course_by_id
log = logging.getLogger(__name__)
def root_create(request):
"""
In the edX wiki, we don't show the root_create view. Instead, we
just create the root automatically if it doesn't exist.
"""
root = get_or_create_root()
return redirect('wiki:get', path=root.path)
def course_wiki_redirect(request, course_id):
"""
This redirects to whatever page on the wiki that the course designates
as it's home page. A course's wiki must be an article on the root (for
example, "/6.002x") to keep things simple.
"""
course = get_course_by_id(course_id)
course_slug = course.wiki_slug
valid_slug = True
if not course_slug:
log.exception("This course is improperly configured. The slug cannot be empty.")
valid_slug = False
if re.match('^[-\w\.]+$', course_slug) == None:
log.exception("This course is improperly configured. The slug can only contain letters, numbers, periods or hyphens.")
valid_slug = False
if not valid_slug:
return redirect("wiki:get", path="")
# The wiki needs a Site object created. We make sure it exists here
try:
site = Site.objects.get_current()
except Site.DoesNotExist:
new_site = Site()
new_site.domain = settings.SITE_NAME
new_site.name = "edX"
new_site.save()
if str(new_site.id) != str(settings.SITE_ID):
raise ImproperlyConfigured("No site object was created and the SITE_ID doesn't match the newly created one. " + str(new_site.id) + "!=" + str(settings.SITE_ID))
try:
urlpath = URLPath.get_by_path(course_slug, select_related=True)
results = list( Article.objects.filter( id = urlpath.article.id ) )
if results:
article = results[0]
else:
article = None
except (NoRootURL, URLPath.DoesNotExist):
# We will create it in the next block
urlpath = None
article = None
if not article:
# create it
root = get_or_create_root()
if urlpath:
# Somehow we got a urlpath without an article. Just delete it and
# recerate it.
urlpath.delete()
urlpath = URLPath.create_article(
root,
course_slug,
title=course.title,
content="This is the wiki for " + course.title + ".",
user_message="Course page automatically created.",
user=None,
ip_address=None,
article_kwargs={'owner': None,
'group': None,
'group_read': True,
'group_write': True,
'other_read': True,
'other_write': True,
})
return redirect("wiki:get", path=urlpath.path)
def get_or_create_root():
"""
Returns the root article, or creates it if it doesn't exist.
"""
try:
root = URLPath.root()
if not root.article:
root.delete()
raise NoRootURL
return root
except NoRootURL:
pass
starting_content = "\n".join((
"Welcome to the edX Wiki",
"===",
"Visit a course wiki to add an article."))
root = URLPath.create_root(title="edX Wiki",
content=starting_content)
article = root.article
article.group = None
article.group_read = True
article.group_write = False
article.other_read = True
article.other_write = False
article.save()
return root
......@@ -187,7 +187,7 @@ class PageLoader(ActivateLoginTestCase):
def unenroll(self, course):
"""Unenroll the currently logged-in user, and check that it worked."""
resp = self.client.post('/change_enrollment', {
'enrollment_action': 'enroll',
'enrollment_action': 'unenroll',
'course_id': course.id,
})
data = parse_json(resp)
......
Subproject commit 484ff1ce497574045e78d4e3ea0ff55ac9b4bd30
......@@ -90,6 +90,7 @@ sys.path.append(REPO_ROOT)
sys.path.append(ASKBOT_ROOT)
sys.path.append(ASKBOT_ROOT / "askbot" / "deps")
sys.path.append(PROJECT_ROOT / 'djangoapps')
sys.path.append(PROJECT_ROOT / 'djangoapps' / 'django-wiki')
sys.path.append(PROJECT_ROOT / 'lib')
sys.path.append(COMMON_ROOT / 'djangoapps')
sys.path.append(COMMON_ROOT / 'lib')
......@@ -131,6 +132,13 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'askbot.user_messages.context_processors.user_messages',#must be before auth
'django.contrib.auth.context_processors.auth', #this is required for admin
'django.core.context_processors.csrf', #necessary for csrf protection
# Added for django-wiki
'django.core.context_processors.media',
'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai',
'course_wiki.course_nav.context_processor',
)
STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB
......@@ -288,6 +296,9 @@ djcelery.setup_loader()
SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True
SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
################################# WIKI ###################################
WIKI_ACCOUNT_HANDLING = False
################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
......@@ -301,9 +312,13 @@ STATICFILES_FINDERS = (
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
'askbot.skins.loaders.filesystem_load_template_source',
'mitxmako.makoloader.MakoFilesystemLoader',
'mitxmako.makoloader.MakoAppDirectoriesLoader',
# 'django.template.loaders.filesystem.Loader',
# 'django.template.loaders.app_directories.Loader',
#'askbot.skins.loaders.filesystem_load_template_source',
# 'django.template.loaders.eggs.Loader',
)
......@@ -319,6 +334,8 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware',
'course_wiki.course_nav.Middleware',
'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware',
'askbot.middleware.forum_mode.ForumModeMiddleware',
......@@ -535,6 +552,15 @@ INSTALLED_APPS = (
'track',
'util',
'certificates',
#For the wiki
'wiki', # The new django-wiki from benjaoming
'course_wiki', # Our customizations
'django_notify',
'mptt',
'sekizai',
'wiki.plugins.attachments',
'wiki.plugins.notifications',
# For testing
'django_jasmine',
......
......@@ -65,5 +65,7 @@ DEBUG_TOOLBAR_PANELS = (
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# problems, but you shouldn't leave it on.
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
'debug_toolbar.panels.profiling.ProfilingDebugPanel',
)
#PIPELINE = True
/* =========================================================
* bootstrap-modal.js v2.0.4
* http://twitter.github.com/bootstrap/javascript.html#modals
* =========================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
!function ($) {
"use strict"; // jshint ;_;
/* MODAL CLASS DEFINITION
* ====================== */
var Modal = function (content, options) {
this.options = options
this.$element = $(content)
.delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
}
Modal.prototype = {
constructor: Modal
, toggle: function () {
return this[!this.isShown ? 'show' : 'hide']()
}
, show: function () {
var that = this
, e = $.Event('show')
this.$element.trigger(e)
if (this.isShown || e.isDefaultPrevented()) return
$('body').addClass('modal-open')
this.isShown = true
escape.call(this)
backdrop.call(this, function () {
var transition = $.support.transition && that.$element.hasClass('fade')
if (!that.$element.parent().length) {
that.$element.appendTo(document.body) //don't move modals dom position
}
that.$element
.show()
if (transition) {
that.$element[0].offsetWidth // force reflow
}
that.$element.addClass('in')
transition ?
that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) :
that.$element.trigger('shown')
})
}
, hide: function (e) {
e && e.preventDefault()
var that = this
e = $.Event('hide')
this.$element.trigger(e)
if (!this.isShown || e.isDefaultPrevented()) return
this.isShown = false
$('body').removeClass('modal-open')
escape.call(this)
this.$element.removeClass('in')
$.support.transition && this.$element.hasClass('fade') ?
hideWithTransition.call(this) :
hideModal.call(this)
}
}
/* MODAL PRIVATE METHODS
* ===================== */
function hideWithTransition() {
var that = this
, timeout = setTimeout(function () {
that.$element.off($.support.transition.end)
hideModal.call(that)
}, 500)
this.$element.one($.support.transition.end, function () {
clearTimeout(timeout)
hideModal.call(that)
})
}
function hideModal(that) {
this.$element
.hide()
.trigger('hidden')
backdrop.call(this)
}
function backdrop(callback) {
var that = this
, animate = this.$element.hasClass('fade') ? 'fade' : ''
if (this.isShown && this.options.backdrop) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(document.body)
if (this.options.backdrop != 'static') {
this.$backdrop.click($.proxy(this.hide, this))
}
if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
this.$backdrop.addClass('in')
doAnimate ?
this.$backdrop.one($.support.transition.end, callback) :
callback()
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in')
$.support.transition && this.$element.hasClass('fade')?
this.$backdrop.one($.support.transition.end, $.proxy(removeBackdrop, this)) :
removeBackdrop.call(this)
} else if (callback) {
callback()
}
}
function removeBackdrop() {
this.$backdrop.remove()
this.$backdrop = null
}
function escape() {
var that = this
if (this.isShown && this.options.keyboard) {
$(document).on('keyup.dismiss.modal', function ( e ) {
e.which == 27 && that.hide()
})
} else if (!this.isShown) {
$(document).off('keyup.dismiss.modal')
}
}
/* MODAL PLUGIN DEFINITION
* ======================= */
$.fn.modal = function (option) {
return this.each(function () {
var $this = $(this)
, data = $this.data('modal')
, options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('modal', (data = new Modal(this, options)))
if (typeof option == 'string') data[option]()
else if (options.show) data.show()
})
}
$.fn.modal.defaults = {
backdrop: true
, keyboard: true
, show: true
}
$.fn.modal.Constructor = Modal
/* MODAL DATA-API
* ============== */
$(function () {
$('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) {
var $this = $(this), href
, $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
, option = $target.data('modal') ? 'toggle' : $.extend({}, $target.data(), $this.data())
e.preventDefault()
$target.modal(option)
})
})
}(window.jQuery);
\ No newline at end of file
div.wiki-wrapper {
display: table;
width: 100%;
section.wiki-body {
@extend .clearfix;
@extend .content;
@include border-radius(0 4px 4px 0);
position: relative;
section.wiki {
padding-top: 25px;
header {
height: 33px;
margin-bottom: 36px;
padding-bottom: 26px;
border-bottom: 1px solid $light-gray;
}
.pull-left {
float: left;
}
.pull-right {
float: right;
}
header {
@extend .topbar;
@include border-radius(0 4px 0 0);
height:46px;
overflow: hidden;
&:empty {
border-bottom: 0;
display: none !important;
}
/*-----------------
Breadcrumbs
-----------------*/
.breadcrumb {
list-style: none;
padding-left: 0;
margin: 0 0 0 flex-gutter();
li {
float: left;
margin-right: 10px;
font-size: 0.9em;
line-height: 31px;
a {
@extend .block-link;
display: inline-block;
max-width: 200px;
overflow: hidden;
height: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
p {
float: left;
line-height: 46px;
margin-bottom: 0;
padding-left: lh();
&:after {
content: '›';
display: inline-block;
margin-left: 10px;
color: $base-font-color;
}
}
}
ul {
float: right;
list-style: none;
li {
float: left;
input[type="button"] {
@extend .block-link;
background-position: 12px center;
background-repeat: no-repeat;
border: 0;
border-left: 1px solid darken(#f6efd4, 20%);
@include border-radius(0);
@include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%));
display: block;
font-size: 12px;
font-weight: normal;
letter-spacing: 1px;
line-height: 46px;
margin: 0;
padding: 0 lh() 0 38px;
text-shadow: none;
text-transform: uppercase;
@include transition();
&.view {
background-image: url('../images/sequence-nav/view.png');
}
&.history {
background-image: url('../images/sequence-nav/history.png');
}
&.edit {
background-image: url('../images/sequence-nav/edit.png');
}
&:hover {
background-color: transparent;
}
}
}
.dropdown-menu {
display: none;
}
/*-----------------
Global Functions
-----------------*/
.global-functions {
display: block;
width: auto;
margin-right: flex-gutter();
}
.add-article-btn {
@include button(simple, #eee);
margin-left: 25px;
padding: 7px 15px !important;
font-size: 0.72em;
font-weight: 600;
}
.search-wiki {
margin-top: 3px;
input {
width: 180px;
height: 27px;
padding: 0 15px 0 35px;
background: url(../images/search-icon.png) no-repeat 9px center #f6f6f6;
border: 1px solid #c8c8c8;
border-radius: 14px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12) inset;
font-family: $sans-serif;
font-size: 12px;
outline: none;
@include transition(border-color .1s);
&:-webkit-input-placholder {
font-style: italic;
}
&:focus {
border-color: $blue;
}
}
}
h2.wiki-title {
@include box-sizing(border-box);
display: inline-block;
float: left;
margin-bottom: 15px;
margin-top: 0;
padding-right: flex-gutter(9);
vertical-align: top;
width: flex-grid(2.5, 9);
@media screen and (max-width:900px) {
border-right: 0;
display: block;
width: auto;
}
@media print {
border-right: 0;
display: block;
width: auto;
}
}
p {
line-height: 1.6em;
/*-----------------
Article
-----------------*/
.article-wrapper {
}
h1 {
font-weight: bold;
letter-spacing: 0;
}
.main-article {
float: left;
width: flex-grid(9);
margin-left: flex-gutter();
color: $base-font-color;
h2 {
padding-bottom: 8px;
margin-bottom: 22px;
border-bottom: 1px solid $light-gray;
font-size: 1.33em;
font-weight: bold;
color: $base-font-color;
text-transform: none;
letter-spacing: 0;
}
section.results {
border-left: 1px dashed #ddd;
@include box-sizing(border-box);
display: inline-block;
float: left;
padding-left: 10px;
width: flex-grid(6.5, 9);
h3 {
margin-top: 40px;
margin-bottom: 20px;
font-weight: bold;
font-size: 1.1em;
}
@media screen and (max-width:900px) {
border: 0;
display: block;
padding-left: 0;
width: 100%;
width: auto;
}
h4 {
@media print {
display: block;
padding: 0;
width: auto;
}
canvas, img {
page-break-inside: avoid;
}
h5 {
}
h6 {
}
ul {
font-size: inherit;
line-height: inherit;
color: inherit;
}
li {
margin-bottom: 15px;
}
}
/*-----------------
Sidebar
-----------------*/
.article-functions {
float: left;
width: flex-grid(2) + flex-gutter();
margin-left: flex-grid(1);
.timestamp {
margin: 4px 0 15px;
padding: 0 0 15px 5px;
border-bottom: 1px solid $light-gray;
.label {
font-size: 0.7em;
color: #aaa;
text-transform: uppercase;
}
ul.article-list {
margin-left: 15px;
width: 100%;
.date {
font-size: 0.9em;
}
}
}
@media screen and (max-width:900px) {
margin-left: 0px;
}
.nav-tabs {
list-style: none;
padding: 0;
margin: 0;
li {
&.active {
a {
color: $blue;
.icon-view {
background-position: -25px 0;
}
.icon-edit {
background-position: -25px -25px;
}
.icon-changes {
background-position: -25px -49px;
}
li {
border-bottom: 1px solid #eee;
list-style: none;
margin: 0;
padding: 10px 0;
.icon-attachments {
background-position: -25px -73px;
}
&:last-child {
border-bottom: 0;
.icon-settings {
background-position: -25px -99px;
}
h3 {
font-size: 18px;
font-weight: normal;
&:hover {
background: none;
}
}
}
}
a {
display: block;
padding: 2px 4px;
border-radius: 3px;
font-size: 0.9em;
line-height: 25px;
color: #8f8f8f;
.icon {
float: left;
display: block;
width: 25px;
height: 25px;
margin-right: 3px;
background: url(../images/wiki-icons.png) no-repeat;
}
.icon-view {
background-position: 0 0;
}
.icon-edit {
background-position: 0 -25px;
}
.icon-changes {
background-position: 0 -49px;
}
.icon-attachments {
background-position: 0 -73px;
}
.icon-settings {
background-position: 0 -99px;
}
&:hover {
background-color: #f6f6f6;
text-decoration: none;
}
}
}
/*-----------------
Alerts
-----------------*/
.alert {
position: relative;
top: -35px;
margin-bottom: 24px;
padding: 8px 12px;
border: 1px solid #EBE8BF;
border-radius: 3px;
background: $yellow;
font-size: 0.9em;
.close {
position: absolute;
right: 12px;
font-size: 1.3em;
top: 6px;
color: #999;
text-decoration: none;
}
}
}
}
\ No newline at end of file
<%page args="active_page" />
## mako
<%page args="active_page=None" />
<%
if active_page == None and active_page_context is not UNDEFINED:
# If active_page is not passed in as an argument, it may be in the context as active_page_context
active_page = active_page_context
def url_class(url):
if url == active_page:
return "active"
......@@ -23,7 +28,7 @@ def url_class(url):
% endif
% endif
% if settings.WIKI_ENABLED:
<li class="wiki"><a href="${reverse('wiki_root', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li>
<li class="wiki"><a href="${reverse('course_wiki', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li>
% endif
% if user.is_authenticated():
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
......@@ -34,4 +39,4 @@ def url_class(url):
</ol>
</div>
</nav>
\ No newline at end of file
</nav>
## mako
<%! from django.core.urlresolvers import reverse %>
<%namespace name='static' file='static_content.html'/>
......
<!DOCTYPE html>
{% load compressed %}{% load sekizai_tags i18n %}{% load url from future %}
<html>
<head>
{% block title %}<title>edX</title>{% endblock %}
<link rel="icon" type="image/x-icon" href="${static.url('images/favicon.ico')}" />
{% compressed_css 'application' %}
{% compressed_js 'main_vendor' %}
{% block headextra %}{% endblock %}
{% render_block "css" %}
<meta name="path_prefix" content="{{MITX_ROOT_URL}}">
</head>
<body class="{% block bodyclass %}{% endblock %}">
{% include "navigation.html" %}
<section class="content-wrapper">
{% block body %}{% endblock %}
{% block bodyextra %}{% endblock %}
</section>
{% include "footer.html" %}
{% compressed_js 'application' %}
{% compressed_js 'module-js' %}
{% render_block "js" %}
</body>
</html>
{% comment %}
This is a django template version of our main page from which all
other pages inherit. This file should be rewritten to reflect any
changes in main.html! Files used by {% include %} can be written
as mako templates.
Inheriting from this file allows us to include apps that use the
django templating system without rewriting all of their views in
mako.
{% endcomment %}
\ No newline at end of file
## mako
## TODO: Split this into two files, one for people who are authenticated, and
## one for people who aren't. Assume a Course object is passed to the former,
## instead of using settings.COURSE_TITLE
......
{% extends "wiki/base.html" %}
{% load wiki_tags i18n %}
{% load url from future %}
{% block pagetitle %}{{ article.current_revision.title }}{% endblock %}
{% block wiki_breadcrumbs %}
{% include "wiki/includes/breadcrumbs.html" %}
{% endblock %}
{% block wiki_contents %}
<div class="article-wrapper">
<article class="main-article">
<h1>{{ article.current_revision.title }}</h1>
{% block wiki_contents_tab %}
{% wiki_render article %}
{% endblock %}
</article>
<div class="article-functions">
<div class="timestamp">
<span class="label">{% trans "Last modified:" %}</span><br />
<span class="date">{{ article.current_revision.modified }}</span>
</div>
<ul class="nav nav-tabs">
{% include "wiki/includes/article_menu.html" %}
</ul>
</div>
</div>
{% endblock %}
{% block footer_prepend %}
<p><em>{% trans "This article was last modified:" %} {{ article.current_revision.modified }}</em></p>
{% endblock %}
{% extends "main_django.html" %}
{% load compressed %}{% load sekizai_tags i18n %}{% load url from future %}
{% block title %}<title>{% block pagetitle %}{% endblock %} | edX Wiki</title>{% endblock %}
{% block headextra %}
{% compressed_css 'course' %}
<script src="{{ STATIC_URL }}js/bootstrap-modal.js"></script>
{% endblock %}
{% block body %}
{% if course %}
{% include "course_navigation.html" with active_page_context="wiki" %}
{% endif %}
<section class="container wiki">
{% block wiki_body %}
{% block wiki_breadcrumbs %}{% endblock %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
<a class="close" data-dismiss="alert" href="#">&times;</a>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block wiki_contents %}{% endblock %}
{% endblock %}
</section>
{% endblock %}
{% load i18n wiki_tags %}{% load url from future %}
{% with selected_tab as selected %}
<li class="{% if selected == "view" %} active{% endif %}">
<a href="{% url 'wiki:get' article_id=article.id path=urlpath.path %}">
<span class="icon icon-view"></span>
{% trans "View" %}
</a>
</li>
<li class="{% if selected == "edit" %} active{% endif %}">
<a href="{% url 'wiki:edit' article_id=article.id path=urlpath.path %}">
<span class="icon icon-edit"></span>
{% trans "Edit" %}
</a>
</li>
<li class="{% if selected == "history" %} active{% endif %}">
<a href="{% url 'wiki:history' article_id=article.id path=urlpath.path %}">
<span class="icon icon-changes"></span>
{% trans "Changes" %}
</a>
</li>
{% for plugin in article_tabs %}
<li class="{% if selected == plugin.slug %} active{% endif %}">
<a href="{% url 'wiki:plugin' slug=plugin.slug article_id=article.id path=urlpath.path %}">
<span class="icon icon-attachments {{ plugin.article_tab.1 }}"></span>
{{ plugin.article_tab.0 }}
</a>
</li>
{% endfor %}
<li class="{% if selected == "settings" %} active{% endif %}">
{% if not user.is_anonymous %}
<a href="{% url 'wiki:settings' article_id=article.id path=urlpath.path %}">
<span class="icon icon-settings"></span>
{% trans "Settings" %}
</a>
{% endif %}
</li>
{% endwith %}
{% load i18n %}{% load url from future %}
{% if urlpath %}
<header>
<ul class="breadcrumb pull-left" class="">
{% for ancestor in urlpath.get_ancestors.all %}
<li><a href="{% url 'wiki:get' path=ancestor.path %}">{{ ancestor.article.current_revision.title }}</a></li>
{% endfor %}
<li class="active"><a href="{% url 'wiki:get' path=urlpath.path %}">{{ article.current_revision.title }}</a></li>
</ul>
<div class="pull-left" style="margin-left: 10px;">
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#" style="padding: 7px;" title="{% trans "Sub-articles for" %} {{ article.current_revision.title }}">
<span class="icon-list"></span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
{% for child in children_slice %}
<li>
<a href="{% url 'wiki:get' path=child.path %}">
{{ child.article.current_revision.title }}
</a>
</li>
{% empty %}
<li><a href="#"><em>{% trans "No sub-articles" %}</em></a></li>
{% endfor %}
{% if children_slice_more %}
<li><a href="#"><em>{% trans "...and more" %}</em></a></li>
{% endif %}
<li class="divider"></li>
<li>
<a href="" onclick="alert('TODO')">{% trans "List sub-pages" %} &raquo;</a>
</li>
</ul>
</div>
</div>
<div class="global-functions pull-right">
<form class="search-wiki pull-left">
<input type="search" placeholder="search wiki" />
</form>
<a class="add-article-btn btn pull-left" href="{% url 'wiki:create' path=urlpath.path %}" style="padding: 7px;">
<span class="icon-plus"></span>
{% trans "Add article" %}
</a>
</div>
</header>
{% endif %}
<!DOCTYPE html>
{% load wiki_tags i18n %}{% load compressed %}
<html>
<head>
{% compressed_css 'course' %}
</head>
<body>
<section class="content-wrapper">
{% if revision %}
<div class="alert alert-info">
<strong>{% trans "Previewing revision" %}:</strong> {{ revision.created }} (#{{ revision.revision_number }}) by {% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
</div>
{% endif %}
{% if merge %}
<div class="alert alert-info">
<strong>{% trans "Previewing merge between" %}:</strong>
{{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user|is_moderator %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
<strong>{% trans "and" %}</strong>
{{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user|is_moderator %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
</div>
{% endif %}
<h1 class="page-header">{{ title }}</h1>
{% wiki_render article content %}
</section>
</body>
</html>
......@@ -156,9 +156,25 @@ if settings.COURSEWARE_ENABLED:
# Multicourse wiki
if settings.WIKI_ENABLED:
urlpatterns += (
url(r'^wiki/', include('simplewiki.urls')),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/wiki/', include('simplewiki.urls')),
from wiki.urls import get_pattern as wiki_pattern
from django_notify.urls import get_pattern as notify_pattern
# Note that some of these urls are repeated in course_wiki.course_nav. Make sure to update
# them together.
urlpatterns += (
# First we include views from course_wiki that we use to override the default views.
# They come first in the urlpatterns so they get resolved first
url('^wiki/create-root/$', 'course_wiki.views.root_create', name='root_create'),
url(r'^wiki/', include(wiki_pattern())),
url(r'^notify/', include(notify_pattern())),
# These urls are for viewing the wiki in the context of a course. They should
# never be returned by a reverse() so they come after the other url patterns
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/course_wiki/?$',
'course_wiki.views.course_wiki_redirect', name="course_wiki"),
url(r'^courses/(?:[^/]+/[^/]+/[^/]+)/wiki/', include(wiki_pattern())),
)
if settings.QUICKEDIT:
......
......@@ -43,4 +43,5 @@ django-robots
django-ses
django-storages
django-threaded-multihost
django-sekizai<0.7
-r repo-requirements.txt
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