553 lines
21 KiB
Python
553 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
from django.conf import settings as settings
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.context_processors import csrf
|
|
from django.core.urlresolvers import reverse
|
|
from django.db.models import Q
|
|
from django.http import HttpResponse, HttpResponseRedirect, Http404
|
|
from django.utils import simplejson
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from mitxmako.shortcuts import render_to_response
|
|
|
|
from courseware.courses import get_opt_course_with_access
|
|
from courseware.access import has_access
|
|
from xmodule.course_module import CourseDescriptor
|
|
from xmodule.modulestore.django import modulestore
|
|
|
|
from models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm
|
|
import wiki_settings
|
|
|
|
|
|
def wiki_reverse(wiki_page, article=None, course=None, namespace=None, args=[], kwargs={}):
|
|
kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'}
|
|
if not 'course_id' in kwargs and course:
|
|
kwargs['course_id'] = course.id
|
|
if not 'article_path' in kwargs and article:
|
|
kwargs['article_path'] = article.get_path()
|
|
if not 'namespace' in kwargs and namespace:
|
|
kwargs['namespace'] = namespace
|
|
return reverse(wiki_page, kwargs=kwargs, args=args)
|
|
|
|
|
|
def update_template_dictionary(dictionary, request=None, course=None, article=None, revision=None):
|
|
if article:
|
|
dictionary['wiki_article'] = article
|
|
dictionary['wiki_title'] = article.title # TODO: What is the title when viewing the article in a course?
|
|
if not course and 'namespace' not in dictionary:
|
|
dictionary['namespace'] = article.namespace.name
|
|
|
|
if course:
|
|
dictionary['course'] = course
|
|
if 'namespace' not in dictionary:
|
|
dictionary['namespace'] = "edX"
|
|
else:
|
|
dictionary['course'] = None
|
|
|
|
if revision:
|
|
dictionary['wiki_article_revision'] = revision
|
|
dictionary['wiki_current_revision_deleted'] = not (revision.deleted == 0)
|
|
|
|
if request:
|
|
dictionary.update(csrf(request))
|
|
|
|
if request and course:
|
|
dictionary['staff_access'] = has_access(request.user, course, 'staff')
|
|
else:
|
|
dictionary['staff_access'] = False
|
|
|
|
|
|
def view(request, article_path, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
(article, err) = get_article(request, article_path, course)
|
|
if err:
|
|
return err
|
|
|
|
perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
d = {}
|
|
update_template_dictionary(d, request, course, article, article.current_revision)
|
|
return render_to_response('simplewiki/simplewiki_view.html', d)
|
|
|
|
|
|
def view_revision(request, revision_number, article_path, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
(article, err) = get_article(request, article_path, course)
|
|
if err:
|
|
return err
|
|
|
|
try:
|
|
revision = Revision.objects.get(counter=int(revision_number), article=article)
|
|
except:
|
|
d = {'wiki_err_norevision': revision_number}
|
|
update_template_dictionary(d, request, course, article)
|
|
return render_to_response('simplewiki/simplewiki_error.html', d)
|
|
|
|
perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True, revision=revision)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
d = {}
|
|
update_template_dictionary(d, request, course, article, revision)
|
|
|
|
return render_to_response('simplewiki/simplewiki_view.html', d)
|
|
|
|
|
|
def root_redirect(request, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
#TODO: Add a default namespace to settings.
|
|
namespace = "edX"
|
|
|
|
try:
|
|
root = Article.get_root(namespace)
|
|
return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id': course_id, 'article_path': root.get_path()}))
|
|
except:
|
|
# If the root is not found, we probably are loading this class for the first time
|
|
# We should make sure the namespace exists so the root article can be created.
|
|
Namespace.ensure_namespace(namespace)
|
|
|
|
err = not_found(request, namespace + '/', course)
|
|
return err
|
|
|
|
|
|
def create(request, article_path, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
article_path_components = article_path.split('/')
|
|
|
|
# Ensure the namespace exists
|
|
if not len(article_path_components) >= 1 or len(article_path_components[0]) == 0:
|
|
d = {'wiki_err_no_namespace': True}
|
|
update_template_dictionary(d, request, course)
|
|
return render_to_response('simplewiki/simplewiki_error.html', d)
|
|
|
|
namespace = None
|
|
try:
|
|
namespace = Namespace.objects.get(name__exact=article_path_components[0])
|
|
except Namespace.DoesNotExist, ValueError:
|
|
d = {'wiki_err_bad_namespace': True}
|
|
update_template_dictionary(d, request, course)
|
|
return render_to_response('simplewiki/simplewiki_error.html', d)
|
|
|
|
# See if the article already exists
|
|
article_slug = article_path_components[1] if len(article_path_components) >= 2 else ''
|
|
#TODO: Make sure the slug only contains legal characters (which is already done a bit by the url regex)
|
|
|
|
try:
|
|
existing_article = Article.objects.get(namespace=namespace, slug__exact=article_slug)
|
|
#It already exists, so we just redirect to view the article
|
|
return HttpResponseRedirect(wiki_reverse("wiki_view", existing_article, course))
|
|
except Article.DoesNotExist:
|
|
#This is good. The article doesn't exist
|
|
pass
|
|
|
|
#TODO: Once we have permissions for namespaces, we should check for create permissions
|
|
#check_permissions(request, #namespace#, check_locked=False, check_write=True, check_deleted=True)
|
|
|
|
if request.method == 'POST':
|
|
f = CreateArticleForm(request.POST)
|
|
if f.is_valid():
|
|
article = Article()
|
|
article.slug = article_slug
|
|
if not request.user.is_anonymous():
|
|
article.created_by = request.user
|
|
article.title = f.cleaned_data.get('title')
|
|
article.namespace = namespace
|
|
a = article.save()
|
|
new_revision = f.save(commit=False)
|
|
if not request.user.is_anonymous():
|
|
new_revision.revision_user = request.user
|
|
new_revision.article = article
|
|
new_revision.save()
|
|
|
|
return HttpResponseRedirect(wiki_reverse("wiki_view", article, course))
|
|
else:
|
|
f = CreateArticleForm(initial={'title': request.GET.get('wiki_article_name', article_slug),
|
|
'contents': _('Headline\n===\n\n')})
|
|
|
|
d = {'wiki_form': f, 'create_article': True, 'namespace': namespace.name}
|
|
update_template_dictionary(d, request, course)
|
|
|
|
return render_to_response('simplewiki/simplewiki_edit.html', d)
|
|
|
|
|
|
def edit(request, article_path, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
(article, err) = get_article(request, article_path, course)
|
|
if err:
|
|
return err
|
|
|
|
# Check write permissions
|
|
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True, check_deleted=False)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
|
|
EditForm = RevisionFormWithTitle
|
|
else:
|
|
EditForm = RevisionForm
|
|
|
|
if request.method == 'POST':
|
|
f = EditForm(request.POST)
|
|
if f.is_valid():
|
|
new_revision = f.save(commit=False)
|
|
new_revision.article = article
|
|
|
|
if request.POST.__contains__('delete'):
|
|
if (article.current_revision.deleted == 1): # This article has already been deleted. Redirect
|
|
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
|
|
new_revision.contents = ""
|
|
new_revision.deleted = 1
|
|
elif not new_revision.get_diff():
|
|
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
|
|
|
|
if not request.user.is_anonymous():
|
|
new_revision.revision_user = request.user
|
|
new_revision.save()
|
|
if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
|
|
new_revision.article.title = f.cleaned_data['title']
|
|
new_revision.article.save()
|
|
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
|
|
else:
|
|
startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n'
|
|
|
|
f = EditForm({'contents': startContents, 'title': article.title})
|
|
|
|
d = {'wiki_form': f}
|
|
update_template_dictionary(d, request, course, article)
|
|
return render_to_response('simplewiki/simplewiki_edit.html', d)
|
|
|
|
|
|
def history(request, article_path, page=1, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
(article, err) = get_article(request, article_path, course)
|
|
if err:
|
|
return err
|
|
|
|
perm_err = check_permissions(request, article, course, check_read=True, check_deleted=False)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
page_size = 10
|
|
|
|
if page == None:
|
|
page = 1
|
|
try:
|
|
p = int(page)
|
|
except ValueError:
|
|
p = 1
|
|
|
|
history = Revision.objects.filter(article__exact=article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article')
|
|
|
|
if request.method == 'POST':
|
|
if request.POST.__contains__('revision'): # They selected a version, but they can be either deleting or changing the version
|
|
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
redirectURL = wiki_reverse('wiki_view', article, course)
|
|
try:
|
|
r = int(request.POST['revision'])
|
|
revision = Revision.objects.get(id=r)
|
|
if request.POST.__contains__('change'):
|
|
article.current_revision = revision
|
|
article.save()
|
|
elif request.POST.__contains__('view'):
|
|
redirectURL = wiki_reverse('wiki_view_revision', course=course,
|
|
kwargs={'revision_number': revision.counter, 'article_path': article.get_path()})
|
|
#The rese of these are admin functions
|
|
elif request.POST.__contains__('delete') and request.user.is_superuser:
|
|
if (revision.deleted == 0):
|
|
revision.adminSetDeleted(2)
|
|
elif request.POST.__contains__('restore') and request.user.is_superuser:
|
|
if (revision.deleted == 2):
|
|
revision.adminSetDeleted(0)
|
|
elif request.POST.__contains__('delete_all') and request.user.is_superuser:
|
|
Revision.objects.filter(article__exact=article, deleted=0).update(deleted=2)
|
|
elif request.POST.__contains__('lock_article'):
|
|
article.locked = not article.locked
|
|
article.save()
|
|
except Exception as e:
|
|
print str(e)
|
|
pass
|
|
finally:
|
|
return HttpResponseRedirect(redirectURL)
|
|
#
|
|
#
|
|
# <input type="submit" name="delete" value="Delete revision"/>
|
|
# <input type="submit" name="restore" value="Restore revision"/>
|
|
# <input type="submit" name="delete_all" value="Delete all revisions">
|
|
# %else:
|
|
# <input type="submit" name="delete_article" value="Delete all revisions">
|
|
#
|
|
|
|
page_count = (history.count() + (page_size - 1)) / page_size
|
|
if p > page_count:
|
|
p = 1
|
|
beginItem = (p - 1) * page_size
|
|
|
|
next_page = p + 1 if page_count > p else None
|
|
prev_page = p - 1 if p > 1 else None
|
|
|
|
d = {'wiki_page': p,
|
|
'wiki_next_page': next_page,
|
|
'wiki_prev_page': prev_page,
|
|
'wiki_history': history[beginItem:beginItem + page_size],
|
|
'show_delete_revision': request.user.is_superuser}
|
|
update_template_dictionary(d, request, course, article)
|
|
|
|
return render_to_response('simplewiki/simplewiki_history.html', d)
|
|
|
|
|
|
def revision_feed(request, page=1, namespace=None, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
page_size = 10
|
|
|
|
if page == None:
|
|
page = 1
|
|
try:
|
|
p = int(page)
|
|
except ValueError:
|
|
p = 1
|
|
|
|
history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision')
|
|
|
|
page_count = (history.count() + (page_size - 1)) / page_size
|
|
if p > page_count:
|
|
p = 1
|
|
beginItem = (p - 1) * page_size
|
|
|
|
next_page = p + 1 if page_count > p else None
|
|
prev_page = p - 1 if p > 1 else None
|
|
|
|
d = {'wiki_page': p,
|
|
'wiki_next_page': next_page,
|
|
'wiki_prev_page': prev_page,
|
|
'wiki_history': history[beginItem:beginItem + page_size],
|
|
'show_delete_revision': request.user.is_superuser,
|
|
'namespace': namespace}
|
|
update_template_dictionary(d, request, course)
|
|
|
|
return render_to_response('simplewiki/simplewiki_revision_feed.html', d)
|
|
|
|
|
|
def search_articles(request, namespace=None, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
# blampe: We should check for the presence of other popular django search
|
|
# apps and use those if possible. Only fall back on this as a last resort.
|
|
# Adding some context to results (eg where matches were) would also be nice.
|
|
|
|
# todo: maybe do some perm checking here
|
|
|
|
if request.method == 'GET':
|
|
querystring = request.GET.get('value', '').strip()
|
|
else:
|
|
querystring = ""
|
|
|
|
results = Article.objects.all()
|
|
if namespace:
|
|
results = results.filter(namespace__name__exact=namespace)
|
|
|
|
if request.user.is_superuser:
|
|
results = results.order_by('current_revision__deleted')
|
|
else:
|
|
results = results.filter(current_revision__deleted=0)
|
|
|
|
if querystring:
|
|
for queryword in querystring.split():
|
|
# Basic negation is as fancy as we get right now
|
|
if queryword[0] == '-' and len(queryword) > 1:
|
|
results._search = lambda x: results.exclude(x)
|
|
queryword = queryword[1:]
|
|
else:
|
|
results._search = lambda x: results.filter(x)
|
|
|
|
results = results._search(Q(current_revision__contents__icontains=queryword) | \
|
|
Q(title__icontains=queryword))
|
|
|
|
results = results.select_related('current_revision__deleted', 'namespace')
|
|
|
|
results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower()))
|
|
|
|
if len(results) == 1 and querystring:
|
|
return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course))
|
|
else:
|
|
d = {'wiki_search_results': results,
|
|
'wiki_search_query': querystring,
|
|
'namespace': namespace}
|
|
update_template_dictionary(d, request, course)
|
|
return render_to_response('simplewiki/simplewiki_searchresults.html', d)
|
|
|
|
|
|
def search_add_related(request, course_id, slug, namespace):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
|
if err:
|
|
return err
|
|
|
|
perm_err = check_permissions(request, article, course, check_read=True)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
search_string = request.GET.get('query', None)
|
|
self_pk = request.GET.get('self', None)
|
|
if search_string:
|
|
results = []
|
|
related = Article.objects.filter(title__istartswith=search_string)
|
|
others = article.related.all()
|
|
if self_pk:
|
|
related = related.exclude(pk=self_pk)
|
|
if others:
|
|
related = related.exclude(related__in=others)
|
|
related = related.order_by('title')[:10]
|
|
for item in related:
|
|
results.append({'id': str(item.id),
|
|
'value': item.title,
|
|
'info': item.get_url()})
|
|
else:
|
|
results = []
|
|
|
|
json = simplejson.dumps({'results': results})
|
|
return HttpResponse(json, mimetype='application/json')
|
|
|
|
|
|
def add_related(request, course_id, slug, namespace):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
|
if err:
|
|
return err
|
|
|
|
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
try:
|
|
related_id = request.POST['id']
|
|
rel = Article.objects.get(id=related_id)
|
|
has_already = article.related.filter(id=related_id).count()
|
|
if has_already == 0 and not rel == article:
|
|
article.related.add(rel)
|
|
article.save()
|
|
except:
|
|
pass
|
|
finally:
|
|
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
|
|
|
|
|
|
def remove_related(request, course_id, namespace, slug, related_id):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
|
|
|
if err:
|
|
return err
|
|
|
|
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
|
|
if perm_err:
|
|
return perm_err
|
|
|
|
try:
|
|
rel_id = int(related_id)
|
|
rel = Article.objects.get(id=rel_id)
|
|
article.related.remove(rel)
|
|
article.save()
|
|
except:
|
|
pass
|
|
finally:
|
|
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
|
|
|
|
|
|
def random_article(request, course_id=None):
|
|
course = get_opt_course_with_access(request.user, course_id, 'load')
|
|
|
|
from random import randint
|
|
num_arts = Article.objects.count()
|
|
article = Article.objects.all()[randint(0, num_arts - 1)]
|
|
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
|
|
|
|
|
|
def not_found(request, article_path, course):
|
|
"""Generate a NOT FOUND message for some URL"""
|
|
d = {'wiki_err_notfound': True,
|
|
'article_path': article_path,
|
|
'namespace': "edX"}
|
|
update_template_dictionary(d, request, course)
|
|
return render_to_response('simplewiki/simplewiki_error.html', d)
|
|
|
|
|
|
def get_article(request, article_path, course):
|
|
err = None
|
|
article = None
|
|
|
|
try:
|
|
article = Article.get_article(article_path)
|
|
except Article.DoesNotExist, ValueError:
|
|
err = not_found(request, article_path, course)
|
|
|
|
return (article, err)
|
|
|
|
|
|
def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision=None):
|
|
read_err = check_read and not article.can_read(request.user)
|
|
|
|
write_err = check_write and not article.can_write(request.user)
|
|
|
|
locked_err = check_locked and article.locked
|
|
|
|
if revision is None:
|
|
revision = article.current_revision
|
|
deleted_err = check_deleted and not (revision.deleted == 0)
|
|
if (request.user.is_superuser):
|
|
deleted_err = False
|
|
locked_err = False
|
|
|
|
if read_err or write_err or locked_err or deleted_err:
|
|
d = {'wiki_article': article,
|
|
'wiki_err_noread': read_err,
|
|
'wiki_err_nowrite': write_err,
|
|
'wiki_err_locked': locked_err,
|
|
'wiki_err_deleted': deleted_err, }
|
|
update_template_dictionary(d, request, course)
|
|
# TODO: Make this a little less jarring by just displaying an error
|
|
# on the current page? (no such redirect happens for an anon upload yet)
|
|
# benjaoming: I think this is the nicest way of displaying an error, but
|
|
# these errors shouldn't occur, but rather be prevented on the other pages.
|
|
return render_to_response('simplewiki/simplewiki_error.html', d)
|
|
else:
|
|
return None
|
|
|
|
####################
|
|
# LOGIN PROTECTION #
|
|
####################
|
|
|
|
|
|
if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW:
|
|
view = login_required(view)
|
|
history = login_required(history)
|
|
search_articles = login_required(search_articles)
|
|
root_redirect = login_required(root_redirect)
|
|
revision_feed = login_required(revision_feed)
|
|
random_article = login_required(random_article)
|
|
search_add_related = login_required(search_add_related)
|
|
not_found = login_required(not_found)
|
|
view_revision = login_required(view_revision)
|
|
|
|
if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT:
|
|
create = login_required(create)
|
|
edit = login_required(edit)
|
|
add_related = login_required(add_related)
|
|
remove_related = login_required(remove_related)
|
|
|
|
if wiki_settings.WIKI_CONTEXT_PREPROCESSORS:
|
|
settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS
|