Commit 2940142e by benjaoming

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

parent 322b5a6a
django-wiki 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 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 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 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 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 Installation
------------ ------------
......
Management script:
Cleanup deleted Image's image files
Cleanup revisions
...@@ -3,19 +3,48 @@ from django.contrib.contenttypes.generic import GenericTabularInline ...@@ -3,19 +3,48 @@ from django.contrib.contenttypes.generic import GenericTabularInline
from mptt.admin import MPTTModelAdmin from mptt.admin import MPTTModelAdmin
import models import models
from django import forms
from django.forms.widgets import HiddenInput
class ArticleObjectAdmin(GenericTabularInline): class ArticleObjectAdmin(GenericTabularInline):
model = models.ArticleForObject model = models.ArticleForObject
extra = 1 extra = 1
max_num = 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): class ArticleAdmin(admin.ModelAdmin):
pass inlines = [ArticleRevisionInline]
form = ArticleForm
class URLPathAdmin(MPTTModelAdmin): class URLPathAdmin(MPTTModelAdmin):
inlines = [ArticleObjectAdmin] inlines = [ArticleObjectAdmin]
list_filter = ('site',) list_filter = ('site', 'articles__article__current_revision__deleted',
list_display = ('slug', 'article') '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.URLPath, URLPathAdmin)
admin.site.register(models.Article, ArticleAdmin) admin.site.register(models.Article, ArticleAdmin)
\ No newline at end of file admin.site.register(models.ArticleRevision, ArticleRevisionAdmin)
\ No newline at end of file
...@@ -4,4 +4,53 @@ from django.conf import settings as django_settings ...@@ -4,4 +4,53 @@ from django.conf import settings as django_settings
# Should urls be case sensitive? # Should urls be case sensitive?
URL_CASE_SENSITIVE = getattr(django_settings, "WIKI_URL_CASE_SENSITIVE", False) URL_CASE_SENSITIVE = getattr(django_settings, "WIKI_URL_CASE_SENSITIVE", False)
APP_LABEL = 'wiki' APP_LABEL = 'wiki'
\ No newline at end of file
# 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): ...@@ -5,4 +5,8 @@ class NoRootURL(Exception):
# If there is more than one... # If there is more than one...
class MultipleRootURLs(Exception): class MultipleRootURLs(Exception):
pass pass
\ No newline at end of file
class IllegalFileExtension(Exception):
"""File extension on upload is not allowed"""
pass
...@@ -4,8 +4,9 @@ from django.conf import settings as django_settings ...@@ -4,8 +4,9 @@ from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
import warnings import warnings
from article import Article, ArticleRevision, ArticleForObject # TODO: Don't use wildcards
from urlpath import URLPath from article import *
from urlpath import *
###################### ######################
# Configuration stuff # Configuration stuff
......
...@@ -12,14 +12,16 @@ from article import Article ...@@ -12,14 +12,16 @@ from article import Article
from wiki.models.article import ArticleRevision, ArticleForObject from wiki.models.article import ArticleRevision, ArticleForObject
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models.signals import pre_delete
class URLPath(MPTTModel): class URLPath(MPTTModel):
""" """
Strategy: Very few fields go here, as most has to be managed through an 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. 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 # Tells django-wiki that permissions from a this object's article
# should be inherited to children's articles # 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 INHERIT_PERMISSIONS = True
articles = generic.GenericRelation(ArticleForObject) articles = generic.GenericRelation(ArticleForObject)
...@@ -27,19 +29,35 @@ class URLPath(MPTTModel): ...@@ -27,19 +29,35 @@ class URLPath(MPTTModel):
site = models.ForeignKey(Site) site = models.ForeignKey(Site)
parent = TreeForeignKey('self', null=True, blank=True, related_name='children') parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
def get_path(self): @property
"/".join([obj.slug for obj in self.get_ancestors(include_self=True)]) 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: class MPTTMeta:
pass pass
def __unicode__(self): def __unicode__(self):
path = self.get_path() path = self.path
return path if path else ugettext(u"(root)") return path if path else ugettext(u"(root)")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(URLPath, self).save(*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: class Meta:
verbose_name = _(u'URL path') verbose_name = _(u'URL path')
verbose_name_plural = _(u'URL paths') verbose_name_plural = _(u'URL paths')
...@@ -52,7 +70,7 @@ class URLPath(MPTTModel): ...@@ -52,7 +70,7 @@ class URLPath(MPTTModel):
if not self.slug and self.parent: if not self.slug and self.parent:
raise ValidationError(_(u'A non-root note must always have a slug.')) raise ValidationError(_(u'A non-root note must always have a slug.'))
if not self.parent: 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) raise ValidationError(_(u'There is already a root node on %s') % self.site)
super(URLPath, self).clean(*args, **kwargs) super(URLPath, self).clean(*args, **kwargs)
...@@ -62,22 +80,15 @@ class URLPath(MPTTModel): ...@@ -62,22 +80,15 @@ class URLPath(MPTTModel):
Strategy: Don't handle all kinds of weird cases. Be strict. Strategy: Don't handle all kinds of weird cases. Be strict.
Accepts paths both starting with and without '/' Accepts paths both starting with and without '/'
""" """
site = Site.objects.get_current()
root_nodes = cls.objects.root_nodes().filter(site=site)
path = path.lstrip("/") path = path.lstrip("/")
no_paths = root_nodes.count()
if no_paths == 0:
raise NoRootURL
if no_paths > 1:
raise MultipleRootURLs
# Root page requested # Root page requested
if not path: if not path:
return root_nodes[0] return cls.root()
slugs = path.split('/') slugs = path.split('/')
level = 1 level = 1
parent = root_nodes[0] parent = cls.root()
for slug in slugs: for slug in slugs:
if settings.URL_CASE_SENSITIVE: if settings.URL_CASE_SENSITIVE:
parent = parent.get_children.get(slug=slug) parent = parent.get_children.get(slug=slug)
...@@ -100,7 +111,39 @@ class URLPath(MPTTModel): ...@@ -100,7 +111,39 @@ class URLPath(MPTTModel):
@property @property
def article(self): def article(self):
try: try:
return self.articles.all()[0] return self.articles.all()[0].article
except IndexError: except IndexError:
return None 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