Commit f9de9623 by Bridger Maxwell

Merge remote-tracking branch 'origin/master' into course_start

parents 60da2304 214d03c1
...@@ -9,6 +9,9 @@ def replace(static_url, prefix=None): ...@@ -9,6 +9,9 @@ def replace(static_url, prefix=None):
prefix = prefix + '/' prefix = prefix + '/'
quote = static_url.group('quote') quote = static_url.group('quote')
if staticfiles_storage.exists(static_url.group('rest')):
return static_url.group(0)
else:
url = staticfiles_storage.url(prefix + static_url.group('rest')) url = staticfiles_storage.url(prefix + static_url.group('rest'))
return "".join([quote, url, quote]) return "".join([quote, url, quote])
...@@ -20,7 +23,7 @@ def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/'): ...@@ -20,7 +23,7 @@ def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/'):
return re.sub(r""" return re.sub(r"""
(?x) # flags=re.VERBOSE (?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes (?P<quote>\\?['"]) # the opening quotes
{prefix} # the prefix (?P<prefix>{prefix}) # the prefix
(?P<rest>.*?) # everything else in the url (?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote (?P=quote) # the first matching closing quote
""".format(prefix=replace_prefix), replace_url, text) """.format(prefix=replace_prefix), replace_url, text)
...@@ -172,12 +172,8 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -172,12 +172,8 @@ class XmlDescriptor(XModuleDescriptor):
log.debug('filepath=%s, resources_fs=%s' % (filepath, system.resources_fs)) log.debug('filepath=%s, resources_fs=%s' % (filepath, system.resources_fs))
try: try:
with system.resources_fs.open(filepath) as file: with system.resources_fs.open(filepath) as file:
try:
definition_xml = cls.file_to_xml(file) definition_xml = cls.file_to_xml(file)
except: except (ResourceNotFoundError, etree.XMLSyntaxError):
log.exception("Failed to parse xml in file %s" % filepath)
raise
except ResourceNotFoundError:
log.exception('Unable to load file contents at path %s' % filepath) log.exception('Unable to load file contents at path %s' % filepath)
return {'data': 'Error loading file contents at path %s' % filepath} return {'data': 'Error loading file contents at path %s' % filepath}
......
import json import json
import logging import logging
import settings
import uuid import uuid
from django.conf import settings from django.conf import settings
...@@ -13,8 +12,9 @@ import courseware.grades as grades ...@@ -13,8 +12,9 @@ import courseware.grades as grades
from certificates.models import GeneratedCertificate, certificate_state_for_student, revoke_certificate from certificates.models import GeneratedCertificate, certificate_state_for_student, revoke_certificate
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from student.models import UserProfile from student.models import UserProfile
from student.survey_questions import exit_survey_list_for_student #TODO: Finish migrating these changes from stable
from student.views import student_took_survey, record_exit_survey # from student.survey_questions import exit_survey_list_for_student
# from student.views import student_took_survey, record_exit_survey
log = logging.getLogger("mitx.certificates") log = logging.getLogger("mitx.certificates")
......
import difflib
import os
from django import forms
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals
from django.utils.translation import ugettext_lazy as _
from markdown import markdown
from wiki_settings import *
from util.cache import cache
class ShouldHaveExactlyOneRootSlug(Exception):
pass
class Article(models.Model):
"""Wiki article referring to Revision model for actual content.
'slug' and 'parent' field should be maintained centrally, since users
aren't allowed to change them, anyways.
"""
title = models.CharField(max_length=512, verbose_name=_('Article title'),
blank=False)
slug = models.SlugField(max_length=100, verbose_name=_('slug'),
help_text=_('Letters, numbers, underscore and hyphen.'),
blank=True)
created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True)
created_on = models.DateTimeField(auto_now_add = 1)
modified_on = models.DateTimeField(auto_now_add = 1)
parent = models.ForeignKey('self', verbose_name=_('Parent article slug'),
help_text=_('Affects URL structure and possibly inherits permissions'),
null=True, blank=True)
locked = models.BooleanField(default=False, verbose_name=_('Locked for editing'))
permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'),
blank=True, null=True,
help_text=_('Permission group'))
current_revision = models.OneToOneField('Revision', related_name='current_rev',
blank=True, null=True, editable=True)
related = models.ManyToManyField('self', verbose_name=_('Related articles'), symmetrical=True,
help_text=_('Sets a symmetrical relation other articles'),
blank=True, null=True)
def attachments(self):
return ArticleAttachment.objects.filter(article__exact = self)
@classmethod
def get_root(cls):
"""Return the root article, which should ALWAYS exist..
except the very first time the wiki is loaded, in which
case the user is prompted to create this article."""
try:
return Article.objects.filter(slug__exact = "")[0]
except:
raise ShouldHaveExactlyOneRootSlug()
def get_url(self):
"""Return the Wiki URL for an article"""
url = self.slug + "/"
if self.parent_id:
parent_url = cache.get("wiki_url-" + str(self.parent_id))
if parent_url is None:
parent_url = self.parent.get_url()
url = parent_url + url
cache.set("wiki_url-" + str(self.id), url, 60*60)
return url
def get_abs_url(self):
"""Return the absolute path for an article. This is necessary in cases
where the template system isn't used for generating URLs..."""
# TODO: Remove and create a reverse() lookup.
return WIKI_BASE + self.get_url()
@models.permalink
def get_absolute_url(self):
return ('wiki_view', [self.get_url()])
@classmethod
def get_url_reverse(cls, path, article, return_list=[]):
"""Lookup a URL and return the corresponding set of articles
in the path."""
if path == []:
return return_list + [article]
# Lookup next child in path
try:
a = Article.objects.get(parent__exact = article, slug__exact=str(path[0]))
return cls.get_url_reverse(path[1:], a, return_list+[article])
except Exception, e:
return None
def can_read(self, user):
""" Check read permissions and return True/False."""
if user.is_superuser:
return True
if self.permissions:
perms = self.permissions.can_read.all()
return perms.count() == 0 or (user in perms)
else:
return self.parent.can_read(user) if self.parent else True
def can_write(self, user):
""" Check write permissions and return True/False."""
if user.is_superuser:
return True
if self.permissions:
perms = self.permissions.can_write.all()
return perms.count() == 0 or (user in perms)
else:
return self.parent.can_write(user) if self.parent else True
def can_write_l(self, user):
"""Check write permissions and locked status"""
if user.is_superuser:
return True
return not self.locked and self.can_write(user)
def can_attach(self, user):
return self.can_write_l(user) and (WIKI_ALLOW_ANON_ATTACHMENTS or not user.is_anonymous())
def __unicode__(self):
if self.slug == '' and not self.parent:
return unicode(_('Root article'))
else:
return self.get_url()
class Meta:
unique_together = (('slug', 'parent'),)
verbose_name = _('Article')
verbose_name_plural = _('Articles')
def get_attachment_filepath(instance, filename):
"""Store file, appending new extension for added security"""
dir_ = WIKI_ATTACHMENTS + instance.article.get_url()
dir_ = '/'.join(filter(lambda x: x!='', dir_.split('/')))
if not os.path.exists(WIKI_ATTACHMENTS_ROOT + dir_):
os.makedirs(WIKI_ATTACHMENTS_ROOT + dir_)
return dir_ + '/' + filename + '.upload'
class ArticleAttachment(models.Model):
article = models.ForeignKey(Article, verbose_name=_('Article'))
file = models.FileField(max_length=255, upload_to=get_attachment_filepath, verbose_name=_('Attachment'))
uploaded_by = models.ForeignKey(User, blank=True, verbose_name=_('Uploaded by'), null=True)
uploaded_on = models.DateTimeField(auto_now_add = True, verbose_name=_('Upload date'))
def download_url(self):
return reverse('wiki_view_attachment', args=(self.article.get_url(), self.filename()))
def filename(self):
return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
def get_size(self):
try:
size = self.file.size
except OSError:
size = 0
return size
def filename(self):
return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
def is_image(self):
fname = self.filename().split('.')
if len(fname) > 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS:
return True
return False
def get_thumb(self):
return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE)
def get_thumb_small(self):
return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL)
def mk_thumbs(self):
self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force':True})
self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force':True})
def mk_thumb(self, width, height, force=False):
"""Requires Python Imaging Library (PIL)"""
if not self.get_size():
return False
if not self.is_image():
return False
base_path = os.path.dirname(self.file.path)
orig_name = self.filename().split('.')
thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename)
if force or not os.path.exists(thumb_filepath):
try:
import Image
img = Image.open(self.file.path)
img.thumbnail((width,height), Image.ANTIALIAS)
img.save(thumb_filepath)
except IOError:
return False
return True
def get_thumb_impl(self, width, height):
"""Requires Python Imaging Library (PIL)"""
if not self.get_size():
return False
if not self.is_image():
return False
self.mk_thumb(width, height)
orig_name = self.filename().split('.')
thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() +'/' + thumb_filename
return thumb_url
def __unicode__(self):
return self.filename()
class Revision(models.Model):
article = models.ForeignKey(Article, verbose_name=_('Article'))
revision_text = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Description of change'))
revision_user = models.ForeignKey(User, verbose_name=_('Modified by'),
blank=True, null=True, related_name='wiki_revision_user')
revision_date = models.DateTimeField(auto_now_add = True, verbose_name=_('Revision date'))
contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)'))
contents_parsed = models.TextField(editable=False, blank=True, null=True)
counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False)
previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False)
# Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should
# be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed
# from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't
# show up in the history.
deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0)
def get_user(self):
return self.revision_user if self.revision_user else _('Anonymous')
# Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in
# save that update the revision or reject the save if contents haven't changed
def adminSetDeleted(self, deleted):
self.deleted = deleted
super(Revision, self).save()
def save(self, **kwargs):
# Check if contents have changed... if not, silently ignore save
if self.article and self.article.current_revision:
if self.deleted == 0 and self.article.current_revision.contents == self.contents:
return
else:
import datetime
self.article.modified_on = datetime.datetime.now()
self.article.save()
# Increment counter according to previous revision
previous_revision = Revision.objects.filter(article=self.article).order_by('-counter')
if previous_revision.count() > 0:
if previous_revision.count() > previous_revision[0].counter:
self.counter = previous_revision.count() + 1
else:
self.counter = previous_revision[0].counter + 1
else:
self.counter = 1
if (self.article.current_revision and self.article.current_revision.deleted == 0):
self.previous_revision = self.article.current_revision
# Create pre-parsed contents - no need to parse on-the-fly
ext = WIKI_MARKDOWN_EXTENSIONS
ext += ["wikipath(base_url=%s)" % reverse('wiki_view', args=('/',))]
self.contents_parsed = markdown(self.contents,
extensions=ext,
safe_mode='escape',)
super(Revision, self).save(**kwargs)
def delete(self, **kwargs):
"""If a current revision is deleted, then regress to the previous
revision or insert a stub, if no other revisions are available"""
article = self.article
if article.current_revision == self:
prev_revision = Revision.objects.filter(article__exact = article,
pk__not = self.pk).order_by('-counter')
if prev_revision:
article.current_revision = prev_revision[0]
article.save()
else:
r = Revision(article=article,
revision_user = article.created_by)
r.contents = unicode(_('Auto-generated stub'))
r.revision_text= unicode(_('Auto-generated stub'))
r.save()
article.current_revision = r
article.save()
super(Revision, self).delete(**kwargs)
def get_diff(self):
if (self.deleted == 1):
yield "Article Deletion"
return
if self.previous_revision:
previous = self.previous_revision.contents.splitlines(1)
else:
previous = []
# Todo: difflib.HtmlDiff would look pretty for our history pages!
diff = difflib.unified_diff(previous, self.contents.splitlines(1))
# let's skip the preamble
diff.next(); diff.next(); diff.next()
for d in diff:
yield d
def __unicode__(self):
return "r%d" % self.counter
class Meta:
verbose_name = _('article revision')
verbose_name_plural = _('article revisions')
class Permission(models.Model):
permission_name = models.CharField(max_length = 255, verbose_name=_('Permission name'))
can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write',
help_text=_('Select none to grant anonymous access.'))
can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read',
help_text=_('Select none to grant anonymous access.'))
def __unicode__(self):
return self.permission_name
class Meta:
verbose_name = _('Article permission')
verbose_name_plural = _('Article permissions')
class RevisionForm(forms.ModelForm):
contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows':8, 'cols':50}))
class Meta:
model = Revision
fields = ['contents', 'revision_text']
class RevisionFormWithTitle(forms.ModelForm):
title = forms.CharField(label=_('Title'))
class Meta:
model = Revision
fields = ['title', 'contents', 'revision_text']
class CreateArticleForm(RevisionForm):
title = forms.CharField(label=_('Title'))
class Meta:
model = Revision
fields = ['title', 'contents',]
def set_revision(sender, *args, **kwargs):
"""Signal handler to ensure that a new revision is always chosen as the
current revision - automatically. It simplifies stuff greatly. Also
stores previous revision for diff-purposes"""
instance = kwargs['instance']
created = kwargs['created']
if created and instance.article:
instance.article.current_revision = instance
instance.article.save()
signals.post_save.connect(set_revision, Revision)
# -*- 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
from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response
from models import Revision, Article, CreateArticleForm, RevisionFormWithTitle, RevisionForm
import wiki_settings
def view(request, wiki_url):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_read=True, check_deleted=True)
if perm_err:
return perm_err
d = {'wiki_article': article,
'wiki_article_revision':article.current_revision,
'wiki_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user),
'wiki_current_revision_deleted' : not (article.current_revision.deleted == 0),
'wiki_title' : article.title + " - MITX 6.002x Wiki"
}
d.update(csrf(request))
return render_to_response('simplewiki_view.html', d)
def view_revision(request, revision_number, wiki_url, revision=None):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
try:
revision = Revision.objects.get(counter=int(revision_number), article=article)
except:
d = {'wiki_article': article,
'wiki_err_norevision': revision_number,}
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
perm_err = check_permissions(request, article, check_read=True, check_deleted=True, revision=revision)
if perm_err:
return perm_err
d = {'wiki_article': article,
'wiki_article_revision':revision,
'wiki_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user),
'wiki_current_revision_deleted' : not (revision.deleted == 0),
}
d.update(csrf(request))
return render_to_response('simplewiki_view.html', d)
def root_redirect(request):
try:
root = Article.get_root()
except:
err = not_found(request, '/')
return err
return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url())))
def create(request, wiki_url):
url_path = get_url_path(wiki_url)
if url_path != [] and url_path[0].startswith('_'):
d = {'wiki_err_keyword': True,
'wiki_url': '/'.join(url_path) }
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
# Lookup path
try:
# Ensure that the path exists...
root = Article.get_root()
# Remove root slug if present in path
if url_path and root.slug == url_path[0]:
url_path = url_path[1:]
path = Article.get_url_reverse(url_path[:-1], root)
if not path:
d = {'wiki_err_noparent': True,
'wiki_url_parent': '/'.join(url_path[:-1]) }
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
perm_err = check_permissions(request, path[-1], check_locked=False, check_write=True, check_deleted=True)
if perm_err:
return perm_err
# Ensure doesn't already exist
article = Article.get_url_reverse(url_path, root)
if article:
return HttpResponseRedirect(reverse('wiki_view', args=(article[-1].get_url(),)))
# TODO: Somehow this doesnt work...
#except ShouldHaveExactlyOneRootSlug, (e):
except:
if Article.objects.filter(parent=None).count() > 0:
return HttpResponseRedirect(reverse('wiki_view', args=('/',)))
# Root not found...
path = []
url_path = [""]
if request.method == 'POST':
f = CreateArticleForm(request.POST)
if f.is_valid():
article = Article()
article.slug = url_path[-1]
if not request.user.is_anonymous():
article.created_by = request.user
article.title = f.cleaned_data.get('title')
if path != []:
article.parent = path[-1]
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()
import django.db as db
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
else:
f = CreateArticleForm(initial={'title':request.GET.get('wiki_article_name', url_path[-1]),
'contents':_('Headline\n===\n\n')})
d = {'wiki_form': f,
'wiki_write': True,
'create_article' : True,
}
d.update(csrf(request))
return render_to_response('simplewiki_edit.html', d)
def edit(request, wiki_url):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
# Check write permissions
perm_err = check_permissions(request, article, 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(reverse('wiki_view', args=(article.get_url(),)))
new_revision.contents = ""
new_revision.deleted = 1
elif not new_revision.get_diff():
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
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(reverse('wiki_view', args=(article.get_url(),)))
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,
'wiki_write': True,
'wiki_article': article,
'wiki_title' : article.title,
'wiki_attachments_write': article.can_attach(request.user),
'create_article' : False,
}
d.update(csrf(request))
return render_to_response('simplewiki_edit.html', d)
def history(request, wiki_url, page=1):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_read=True, check_deleted=False)
if perm_err:
print "returned error " , perm_err
return perm_err
page_size = 10
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 wiki_settings.WIKI_REQUIRE_LOGIN_EDIT and not request.user.is_authenticated():
return HttpResponseRedirect('/')
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, check_write=True, check_locked=True)
if perm_err:
return perm_err
redirectURL = reverse('wiki_view', args=(article.get_url(),))
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 = reverse('wiki_view_revision', args=(revision.counter, article.get_url(),))
#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'):
print "changing locked article " , article.locked
article.locked = not article.locked
print "changed locked article " , article.locked
article.save()
except:
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_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user),
'wiki_article': article,
'wiki_title': article.title,
'wiki_history': history[beginItem:beginItem+page_size],
'show_delete_revision' : request.user.is_superuser,}
d.update(csrf(request))
return render_to_response('simplewiki_history.html', d)
def revision_feed(request, page=1):
page_size = 10
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,}
d.update(csrf(request))
return render_to_response('simplewiki_revision_feed.html', d)
def search_articles(request):
# 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 == 'POST':
querystring = request.POST['value'].strip()
else:
querystring = ""
results = Article.objects.all()
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')
results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_url().lower()) )
if len(results) == 1 and querystring:
return HttpResponseRedirect(reverse('wiki_view', args=(results[0].get_url(),)))
else:
d = {'wiki_search_results': results,
'wiki_search_query': querystring,}
d.update(csrf(request))
return render_to_response('simplewiki_searchresults.html', d)
def search_add_related(request, wiki_url):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, 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, wiki_url):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, 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, wiki_url, related_id):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, 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):
from random import randint
num_arts = Article.objects.count()
article = Article.objects.all()[randint(0, num_arts-1)]
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def encode_err(request, url):
d = {'wiki_err_encode': True}
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
def not_found(request, wiki_url):
"""Generate a NOT FOUND message for some URL"""
d = {'wiki_err_notfound': True,
'wiki_url': wiki_url}
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
def get_url_path(url):
"""Return a list of all actual elements of a url, safely ignoring
double-slashes (//) """
return filter(lambda x: x!='', url.split('/'))
def fetch_from_url(request, url):
"""Analyze URL, returning the article and the articles in its path
If something goes wrong, return an error HTTP response"""
err = None
article = None
path = None
url_path = get_url_path(url)
try:
root = Article.get_root()
except:
err = not_found(request, '/')
return (article, path, err)
if url_path and root.slug == url_path[0]:
url_path = url_path[1:]
path = Article.get_url_reverse(url_path, root)
if not path:
err = not_found(request, '/' + '/'.join(url_path))
else:
article = path[-1]
return (article, path, err)
def check_permissions(request, article, 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 == 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,}
d.update(csrf(request))
# 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_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
import os
from django.contrib.auth.decorators import login_required
from django.core.servers.basehttp import FileWrapper
from django.db.models.fields.files import FieldFile
from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.template import loader, Context
from models import ArticleAttachment, get_attachment_filepath
from views import check_permissions, fetch_from_url
from wiki_settings import (
WIKI_ALLOW_ANON_ATTACHMENTS,
WIKI_ALLOW_ATTACHMENTS,
WIKI_ATTACHMENTS_MAX,
WIKI_ATTACHMENTS_ROOT,
WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS,
WIKI_REQUIRE_LOGIN_VIEW,
WIKI_REQUIRE_LOGIN_EDIT,
)
def add_attachment(request, wiki_url):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_write=True, check_locked=True)
if perm_err:
return perm_err
if not WIKI_ALLOW_ATTACHMENTS or (not WIKI_ALLOW_ANON_ATTACHMENTS and request.user.is_anonymous()):
return HttpResponseForbidden()
if request.method == 'POST':
if request.FILES.__contains__('attachment'):
attachment = ArticleAttachment()
if not request.user.is_anonymous():
attachment.uploaded_by = request.user
attachment.article = article
file = request.FILES['attachment']
file_rel_path = get_attachment_filepath(attachment, file.name)
chunk_size = request.upload_handlers[0].chunk_size
filefield = FieldFile(attachment, attachment.file, file_rel_path)
attachment.file = filefield
file_path = WIKI_ATTACHMENTS_ROOT + attachment.file.name
if not request.POST.__contains__('overwrite') and os.path.exists(file_path):
c = Context({'overwrite_warning' : True,
'wiki_article': article,
'filename': file.name})
t = loader.get_template('simplewiki_updateprogressbar.html')
return HttpResponse(t.render(c))
if file.size > WIKI_ATTACHMENTS_MAX:
c = Context({'too_big' : True,
'max_size': WIKI_ATTACHMENTS_MAX,
'wiki_article': article,
'file': file})
t = loader.get_template('simplewiki_updateprogressbar.html')
return HttpResponse(t.render(c))
def get_extension(fname):
return attachment.file.name.split('.')[-2]
if WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS and not \
get_extension(attachment.file.name) in WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS:
c = Context({'extension_err' : True,
'extensions': WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS,
'wiki_article': article,
'file': file})
t = loader.get_template('simplewiki_updateprogressbar.html')
return HttpResponse(t.render(c))
# Remove existing attachments
# TODO: Move this until AFTER having removed file.
# Current problem is that Django's FileField delete() method
# automatically deletes files
for a in article.attachments():
if file_rel_path == a.file.name:
a.delete()
def receive_file():
destination = open(file_path, 'wb+')
size = file.size
cnt = 0
c = Context({'started' : True,})
t = loader.get_template('simplewiki_updateprogressbar.html')
yield t.render(c)
for chunk in file.chunks():
cnt += 1
destination.write(chunk)
c = Context({'progress_width' : (cnt*chunk_size) / size,
'wiki_article': article,})
t = loader.get_template('simplewiki_updateprogressbar.html')
yield t.render(c)
c = Context({'finished' : True,
'wiki_article': article,})
t = loader.get_template('simplewiki_updateprogressbar.html')
destination.close()
attachment.save()
yield t.render(c)
return HttpResponse(receive_file())
return HttpResponse('')
# Taken from http://www.djangosnippets.org/snippets/365/
def send_file(request, filepath):
"""
Send a file through Django without loading the whole file into
memory at once. The FileWrapper will turn the file object into an
iterator for chunks of 8KB.
"""
filename = filepath
wrapper = FileWrapper(file(filename))
response = HttpResponse(wrapper, content_type='text/plain')
response['Content-Length'] = os.path.getsize(filename)
return response
def view_attachment(request, wiki_url, file_name):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_read=True)
if perm_err:
return perm_err
attachment = None
for a in article.attachments():
if get_attachment_filepath(a, file_name) == a.file.name:
attachment = a
if attachment:
filepath = WIKI_ATTACHMENTS_ROOT + attachment.file.name
if os.path.exists(filepath):
return send_file(request, filepath)
raise Http404()
####################
# LOGIN PROTECTION #
####################
if WIKI_REQUIRE_LOGIN_VIEW:
view_attachment = login_required(view_attachment)
if WIKI_REQUIRE_LOGIN_EDIT or not WIKI_ALLOW_ANON_ATTACHMENTS:
add_attachment = login_required(add_attachment)
...@@ -186,7 +186,7 @@ if os.path.isdir(DATA_DIR): ...@@ -186,7 +186,7 @@ if os.path.isdir(DATA_DIR):
# should no longer be added to STATICFILES # should no longer be added to STATICFILES
(course_dir, DATA_DIR / course_dir) (course_dir, DATA_DIR / course_dir)
for course_dir in os.listdir(DATA_DIR) for course_dir in os.listdir(DATA_DIR)
if os.path.isdir(course_dir) if os.path.isdir(DATA_DIR / course_dir)
] ]
# Locale/Internationalization # Locale/Internationalization
......
...@@ -89,57 +89,57 @@ nav.sequence-nav { ...@@ -89,57 +89,57 @@ nav.sequence-nav {
//video //video
&.seq_video_inactive { &.seq_video_inactive {
@extend .inactive; @extend .inactive;
background-image: url('../../images/sequence-nav/video-icon-normal.png'); background-image: url('../images/sequence-nav/video-icon-normal.png');
background-position: center; background-position: center;
} }
&.seq_video_visited { &.seq_video_visited {
@extend .visited; @extend .visited;
background-image: url('../../images/sequence-nav/video-icon-visited.png'); background-image: url('../images/sequence-nav/video-icon-visited.png');
background-position: center; background-position: center;
} }
&.seq_video_active { &.seq_video_active {
@extend .active; @extend .active;
background-image: url('../../images/sequence-nav/video-icon-current.png'); background-image: url('../images/sequence-nav/video-icon-current.png');
background-position: center; background-position: center;
} }
//other //other
&.seq_other_inactive { &.seq_other_inactive {
@extend .inactive; @extend .inactive;
background-image: url('../../images/sequence-nav/document-icon-normal.png'); background-image: url('../images/sequence-nav/document-icon-normal.png');
background-position: center; background-position: center;
} }
&.seq_other_visited { &.seq_other_visited {
@extend .visited; @extend .visited;
background-image: url('../../images/sequence-nav/document-icon-visited.png'); background-image: url('../images/sequence-nav/document-icon-visited.png');
background-position: center; background-position: center;
} }
&.seq_other_active { &.seq_other_active {
@extend .active; @extend .active;
background-image: url('../../images/sequence-nav/document-icon-current.png'); background-image: url('../images/sequence-nav/document-icon-current.png');
background-position: center; background-position: center;
} }
//vertical & problems //vertical & problems
&.seq_vertical_inactive, &.seq_problem_inactive { &.seq_vertical_inactive, &.seq_problem_inactive {
@extend .inactive; @extend .inactive;
background-image: url('../../images/sequence-nav/list-icon-normal.png'); background-image: url('../images/sequence-nav/list-icon-normal.png');
background-position: center; background-position: center;
} }
&.seq_vertical_visited, &.seq_problem_visited { &.seq_vertical_visited, &.seq_problem_visited {
@extend .visited; @extend .visited;
background-image: url('../../images/sequence-nav/list-icon-visited.png'); background-image: url('../images/sequence-nav/list-icon-visited.png');
background-position: center; background-position: center;
} }
&.seq_vertical_active, &.seq_problem_active { &.seq_vertical_active, &.seq_problem_active {
@extend .active; @extend .active;
background-image: url('../../images/sequence-nav/list-icon-current.png'); background-image: url('../images/sequence-nav/list-icon-current.png');
background-position: center; background-position: center;
} }
...@@ -229,7 +229,7 @@ nav.sequence-nav { ...@@ -229,7 +229,7 @@ nav.sequence-nav {
&.prev { &.prev {
a { a {
background-image: url('../../images/sequence-nav/previous-icon.png'); background-image: url('../images/sequence-nav/previous-icon.png');
&:hover { &:hover {
background-color: $cream; background-color: $cream;
...@@ -239,7 +239,7 @@ nav.sequence-nav { ...@@ -239,7 +239,7 @@ nav.sequence-nav {
&.next { &.next {
a { a {
background-image: url('../../images/sequence-nav/next-icon.png'); background-image: url('../images/sequence-nav/next-icon.png');
&:hover { &:hover {
background-color: $cream; background-color: $cream;
...@@ -310,7 +310,7 @@ section.course-content { ...@@ -310,7 +310,7 @@ section.course-content {
&.prev { &.prev {
a { a {
background-image: url('../../images/sequence-nav/previous-icon.png'); background-image: url('../images/sequence-nav/previous-icon.png');
border-right: 1px solid darken(#f6efd4, 20%); border-right: 1px solid darken(#f6efd4, 20%);
&:hover { &:hover {
...@@ -321,7 +321,7 @@ section.course-content { ...@@ -321,7 +321,7 @@ section.course-content {
&.next { &.next {
a { a {
background-image: url('../../images/sequence-nav/next-icon.png'); background-image: url('../images/sequence-nav/next-icon.png');
&:hover { &:hover {
background-color: none; background-color: none;
......
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