Commit 2940142e by benjaoming

More work on models. Add Attachment model, and let attachments pass through a revision system.

parent 322b5a6a
django-wiki
===========
*2012-07-23*
*2012-07-27*
This is where it all begins. In 4 weeks we should have a wiki system appealing to any kind of Django developer out there. Here is the manifest (so far):
This is where it all begins. In less than 4 weeks we should have a wiki system appealing to any kind of Django developer out there. Here is the manifest (so far):
* **Be pluggable and light-weight.** Don't integrate optional features in the core.
* **Be open.** Make an extension API that allows the ecology of the wiki to grow. After all, Wikipedia consists of some [680 extensions](http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/) written for MediaWiki.
* **Be smart.** [This is](https://upload.wikimedia.org/wikipedia/commons/8/88/MediaWiki_database_schema_1-19_%28r102798%29.png) the map of tables in MediaWiki - we'll understand the choices of other wiki projects and make our own. After-all, this is a Django project.
* **Be simple.** The source code should explain itself.
* **Be structured.** Markdown is a simple syntax for readability. Features should be implemented either through easy coding patterns in the content field, but rather stored in a structured way (in the database) and managed through a friendly interface. This gives control back to the website developer, and makes knowledge more usable. Just ask: Why has Wikipedia never changed? Answer: Because it's knowledge is stored in a complicated way, thus it becomes very static.
Installation
------------
......
Management script:
Cleanup deleted Image's image files
Cleanup revisions
......@@ -3,19 +3,48 @@ from django.contrib.contenttypes.generic import GenericTabularInline
from mptt.admin import MPTTModelAdmin
import models
from django import forms
from django.forms.widgets import HiddenInput
class ArticleObjectAdmin(GenericTabularInline):
model = models.ArticleForObject
extra = 1
max_num = 1
class ArticleForm(forms.ModelForm):
class Meta:
model = models.Article
def __init__(self, *args, **kwargs):
super(ArticleForm, self).__init__(*args, **kwargs)
if self.instance.pk:
revisions = models.ArticleRevision.objects.filter(article=self.instance)
self.fields['current_revision'].queryset = revisions
else:
self.fields['current_revision'].queryset = models.ArticleRevision.objects.get_empty_query_set()
self.fields['current_revision'].widget = HiddenInput()
class ArticleRevisionInline(admin.TabularInline):
model = models.ArticleRevision
fk_name = 'article'
extra = 1
fields = ('content', 'title', 'user', 'user_message', 'deleted', 'locked', 'redirect')
class ArticleAdmin(admin.ModelAdmin):
pass
inlines = [ArticleRevisionInline]
form = ArticleForm
class URLPathAdmin(MPTTModelAdmin):
inlines = [ArticleObjectAdmin]
list_filter = ('site',)
list_display = ('slug', 'article')
list_filter = ('site', 'articles__article__current_revision__deleted',
'articles__article__current_revision__created',
'articles__article__modified')
list_display = ('__unicode__', 'article', 'created')
class ArticleRevisionAdmin(admin.ModelAdmin):
pass
admin.site.register(models.URLPath, URLPathAdmin)
admin.site.register(models.Article, ArticleAdmin)
\ No newline at end of file
admin.site.register(models.Article, ArticleAdmin)
admin.site.register(models.ArticleRevision, ArticleRevisionAdmin)
\ No newline at end of file
......@@ -4,4 +4,53 @@ from django.conf import settings as django_settings
# Should urls be case sensitive?
URL_CASE_SENSITIVE = getattr(django_settings, "WIKI_URL_CASE_SENSITIVE", False)
APP_LABEL = 'wiki'
\ No newline at end of file
APP_LABEL = 'wiki'
# This slug is used in URLPath if an article has been deleted. The children of the
# URLPath of that article are moved to lost and found. They keep their permissions
# and all their content.
LOST_AND_FOUND_SLUG = getattr(django_settings, "WIKI_LOST_AND_FOUND_SLUG", 'lost-and-found')
# Where to store article attachments, relative to MEDIA_ROOT
UPLOAD_PATH = getattr(django_settings, "WIKI_UPLOAD_PATH", 'wiki/uploads/%aid/')
# Should the upload path be obscurified? If so, a random hash will be added to the path
# such that someone can not guess the location of files (if you have
# restricted permissions and the files are still located within the web server's
UPLOAD_PATH_OBSCURIFY = getattr(django_settings, "WIKI_UPLOAD_PATH_OBSCURIFY", True)
# Allowed non-image extensions. Empty to disallow completely.
# No files are saved without appending ".upload" to the file to ensure that
# your web server never actually executes some script.
# Case insensitive.
FILE_EXTENTIONS = getattr(django_settings, "WIKI_FILE_EXTENTIONS", ['pdf', 'doc', 'odt', 'docx', 'txt'])
# Where to store images
IMAGE_PATH = getattr(django_settings, "WIKI_IMAGE_PATH", 'wiki/images/%aid/')
####################
# PLANNED SETTINGS #
####################
# Maximum revisions to keep for an article, 0=unlimited
MAX_REVISIONS = getattr(django_settings, "WIKI_MAX_REVISIONS", 100)
# Maximum age of revisions in days, 0=unlimited
MAX_REVISION_AGE = getattr(django_settings, "MAX_REVISION_AGE", 365)
LOG_IPS_ANONYMOUS = getattr(django_settings, "WIKI_LOG_IPS_ANONYMOUS", True)
LOG_IPS_USERS = getattr(django_settings, "WIKI_LOG_IPS_USERS", False)
# Maximum allowed revisions per minute for any given user or IP
REVISIONS_PER_MINUTE = getattr(django_settings, "WIKI_REVISIONS_PER_MINUTE", 3)
# Allow others to upload
UPLOAD_OTHERS = getattr(django_settings, "WIKI_UPLOAD_OTHERS", True)
# Treat anonymous (non logged in) users as the "other" user group
ANONYMOUS = getattr(django_settings, "WIKI_ANONYMOUS", True)
# Globally enable write access for anonymous users, if true anonymous users will be treated
# as the others_write boolean field on models.Article.
ANONYMOUS_WRITE = getattr(django_settings, "WIKI_ANONYMOUS_WRITE", False)
......@@ -5,4 +5,8 @@ class NoRootURL(Exception):
# If there is more than one...
class MultipleRootURLs(Exception):
pass
\ No newline at end of file
pass
class IllegalFileExtension(Exception):
"""File extension on upload is not allowed"""
pass
......@@ -4,8 +4,9 @@ from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
import warnings
from article import Article, ArticleRevision, ArticleForObject
from urlpath import URLPath
# TODO: Don't use wildcards
from article import *
from urlpath import *
######################
# Configuration stuff
......
......@@ -12,14 +12,16 @@ from article import Article
from wiki.models.article import ArticleRevision, ArticleForObject
from django.contrib.contenttypes import generic
from django.core.exceptions import ValidationError
from django.db.models.signals import pre_delete
class URLPath(MPTTModel):
"""
Strategy: Very few fields go here, as most has to be managed through an
article's revision. As a side-effect, the URL resolution remains slim and swift.
"""
# Tells django-wiki that permissions from a URLPath object's article
# should be inherited to children's articles
# Tells django-wiki that permissions from a this object's article
# should be inherited to children's articles. In this case, it's a static
# property.. but you can also use a BooleanField.
INHERIT_PERMISSIONS = True
articles = generic.GenericRelation(ArticleForObject)
......@@ -27,19 +29,35 @@ class URLPath(MPTTModel):
site = models.ForeignKey(Site)
parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
def get_path(self):
"/".join([obj.slug for obj in self.get_ancestors(include_self=True)])
@property
def path(self):
return "/".join([obj.slug for obj in self.get_ancestors(include_self=True)])
@classmethod
def root(cls):
site = Site.objects.get_current()
root_nodes = cls.objects.root_nodes().filter(site=site)
no_paths = root_nodes.count()
if no_paths == 0:
raise NoRootURL
if no_paths > 1:
raise MultipleRootURLs
return root_nodes[0]
class MPTTMeta:
pass
def __unicode__(self):
path = self.get_path()
path = self.path
return path if path else ugettext(u"(root)")
def save(self, *args, **kwargs):
super(URLPath, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
assert not self.parent and self.get_children(), "You cannot delete a root article with children."
super(URLPath, self).delete(*args, **kwargs)
class Meta:
verbose_name = _(u'URL path')
verbose_name_plural = _(u'URL paths')
......@@ -52,7 +70,7 @@ class URLPath(MPTTModel):
if not self.slug and self.parent:
raise ValidationError(_(u'A non-root note must always have a slug.'))
if not self.parent:
if URLPath.objects.root_nodes().filter(site=self.site):
if URLPath.objects.root_nodes().filter(site=self.site).exclude(id=self.id):
raise ValidationError(_(u'There is already a root node on %s') % self.site)
super(URLPath, self).clean(*args, **kwargs)
......@@ -62,22 +80,15 @@ class URLPath(MPTTModel):
Strategy: Don't handle all kinds of weird cases. Be strict.
Accepts paths both starting with and without '/'
"""
site = Site.objects.get_current()
root_nodes = cls.objects.root_nodes().filter(site=site)
path = path.lstrip("/")
no_paths = root_nodes.count()
if no_paths == 0:
raise NoRootURL
if no_paths > 1:
raise MultipleRootURLs
# Root page requested
if not path:
return root_nodes[0]
return cls.root()
slugs = path.split('/')
level = 1
parent = root_nodes[0]
parent = cls.root()
for slug in slugs:
if settings.URL_CASE_SENSITIVE:
parent = parent.get_children.get(slug=slug)
......@@ -100,7 +111,39 @@ class URLPath(MPTTModel):
@property
def article(self):
try:
return self.articles.all()[0]
return self.articles.all()[0].article
except IndexError:
return None
\ No newline at end of file
######################################################
# SIGNAL HANDLERS
######################################################
def on_article_delete(instance, *args, **kwargs):
# If an article is deleted, then throw out its URLPaths
# But move all descendants to a lost-and-found node.
site = Site.objects.get_current()
try:
lost_and_found = URLPath.objects.get(slug=settings.LOST_AND_FOUND_SLUG,
parent=URLPath.root(),
site=site)
except URLPath.DoesNotExist:
lost_and_found = URLPath.objects.create(slug=settings.LOST_AND_FOUND_SLUG,
parent=URLPath.root(),
site=site,)
article = Article(title=_(u"Lost and found"),
group_read = True,
group_write = False,
other_read = False,
other_write = False)
article.add_revision(ArticleRevision(
content=_(u'Articles who lost their parents'
'===============================')))
for urlpath in URLPath.objects.filter(articles__article=article, site=site):
for child in urlpath.get_children():
child.move_to(lost_and_found)
pre_delete.connect(on_article_delete, Article)
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