Commit 322b5a6a by benjaoming

Finalizing URLPath as an MPTT model and generic relations on Articles

parent 83fe3d46
from django.contrib import admin
from django.contrib.contenttypes.generic import GenericTabularInline
from mptt.admin import MPTTModelAdmin
import models
class ArticleObjectAdmin(GenericTabularInline):
model = models.ArticleForObject
extra = 1
max_num = 1
class ArticleAdmin(admin.ModelAdmin):
pass
class URLPathAdmin(MPTTModelAdmin):
inlines = [ArticleObjectAdmin]
list_filter = ('site',)
list_display = ('slug', 'article')
admin.site.register(models.URLPath, URLPathAdmin)
admin.site.register(models.Article, ArticleAdmin)
\ No newline at end of file
......@@ -13,17 +13,27 @@ class Migration(SchemaMigration):
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('title', self.gf('django.db.models.fields.CharField')(max_length=512)),
('current_revision', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='current_set', null=True, to=orm['wiki.ArticleRevision'])),
('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'], null=True, blank=True)),
('group_read', self.gf('django.db.models.fields.BooleanField')(default=True)),
('group_write', self.gf('django.db.models.fields.BooleanField')(default=True)),
('other_read', self.gf('django.db.models.fields.BooleanField')(default=True)),
('other_write', self.gf('django.db.models.fields.BooleanField')(default=True)),
))
db.send_create_signal('wiki', ['Article'])
# Adding model 'ObjectForArticle'
db.create_table('wiki_objectforarticle', (
# Adding model 'ArticleForObject'
db.create_table('wiki_articleforobject', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wiki.Article'])),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_type_set_for_objectforarticle', to=orm['contenttypes.ContentType'])),
('object_pk', self.gf('django.db.models.fields.TextField')()),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_type_set_for_articleforobject', to=orm['contenttypes.ContentType'])),
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
('has_parent_method', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal('wiki', ['ObjectForArticle'])
db.send_create_signal('wiki', ['ArticleForObject'])
# Adding unique constraint on 'ArticleForObject', fields ['content_type', 'object_id']
db.create_unique('wiki_articleforobject', ['content_type_id', 'object_id'])
# Adding model 'ArticleRevision'
db.create_table('wiki_articlerevision', (
......@@ -35,14 +45,15 @@ class Migration(SchemaMigration):
('redirect', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='redirect_set', null=True, to=orm['wiki.Article'])),
('ip_address', self.gf('django.db.models.fields.IPAddressField')(max_length=15, null=True, blank=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('wiki', ['ArticleRevision'])
# Adding model 'URLPath'
db.create_table('wiki_urlpath', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wiki.Article'])),
('slug', self.gf('django.db.models.fields.SlugField')(max_length=50)),
('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, null=True, blank=True)),
('site', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['sites.Site'])),
('parent', self.gf('mptt.fields.TreeForeignKey')(blank=True, related_name='children', null=True, to=orm['wiki.URLPath'])),
('lft', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)),
......@@ -59,11 +70,14 @@ class Migration(SchemaMigration):
# Removing unique constraint on 'URLPath', fields ['site', 'parent', 'slug']
db.delete_unique('wiki_urlpath', ['site_id', 'parent_id', 'slug'])
# Removing unique constraint on 'ArticleForObject', fields ['content_type', 'object_id']
db.delete_unique('wiki_articleforobject', ['content_type_id', 'object_id'])
# Deleting model 'Article'
db.delete_table('wiki_article')
# Deleting model 'ObjectForArticle'
db.delete_table('wiki_objectforarticle')
# Deleting model 'ArticleForObject'
db.delete_table('wiki_articleforobject')
# Deleting model 'ArticleRevision'
db.delete_table('wiki_articlerevision')
......@@ -117,37 +131,45 @@ class Migration(SchemaMigration):
'wiki.article': {
'Meta': {'object_name': 'Article'},
'current_revision': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'current_set'", 'null': 'True', 'to': "orm['wiki.ArticleRevision']"}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
'group_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'group_write': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'other_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'other_write': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'wiki.articleforobject': {
'Meta': {'unique_together': "(('content_type', 'object_id'),)", 'object_name': 'ArticleForObject'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Article']"}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_articleforobject'", 'to': "orm['contenttypes.ContentType']"}),
'has_parent_method': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
},
'wiki.articlerevision': {
'Meta': {'object_name': 'ArticleRevision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Article']"}),
'content': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'redirect': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'redirect_set'", 'null': 'True', 'to': "orm['wiki.Article']"}),
'revision_number': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
'wiki.objectforarticle': {
'Meta': {'object_name': 'ObjectForArticle'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Article']"}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_objectforarticle'", 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_pk': ('django.db.models.fields.TextField', [], {})
},
'wiki.urlpath': {
'Meta': {'unique_together': "(('site', 'parent', 'slug'),)", 'object_name': 'URLPath'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Article']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['wiki.URLPath']"}),
'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
}
}
......
......@@ -4,7 +4,7 @@ from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
import warnings
from article import Article, ArticleRevision, ObjectForArticle
from article import Article, ArticleRevision, ArticleForObject
from urlpath import URLPath
######################
......
......@@ -15,15 +15,6 @@ class Article(models.Model):
verbose_name=_(u'current revision'),
blank=True, null=True, related_name='current_set')
# Permissions. If nothing is set, the article will inherit from
# some other parent, whatever the semantics dictate. For instance, using
# URLPaths means that the article inherits from its URLPath parent.
# Inheriting permissions requires a "get_parent_articles" method to exist on
# one of the objects related to the article.
# TIP: The related object with a get_parent_articles method should be an
# MPTTModel inheritor for efficiency. See the URLPath model.
owner = models.ForeignKey(User, verbose_name=_('owner'),
blank=True, null=True)
......@@ -36,10 +27,53 @@ class Article(models.Model):
other_write = models.BooleanField(default=True)
def can_read(self, user=None, group=None):
return True
if self.other_read:
return True
if user == self.owner:
return True
if self.group_read:
if group == self.group:
return True
if self.group and user and user.groups.filter(group=group):
return True
return False
def can_write(self, user=None, group=None):
return True
if self.other_write:
return True
if user == self.owner:
return True
if self.group_write:
if group == self.group:
return True
if self.group and user and user.groups.filter(group=group):
return True
return False
def decendant_objects(self):
for obj in self.objectforarticle_set.filter(has_parent_field=True):
for decendant in obj.get_decendants():
yield decendant
# All recursive permission methods will use decendant_objects to access
# generic relations and check if they are using MPTT and have INHERIT_PERMISSIONS=True
def set_permissions_recursive(self):
for decendant in self.decendant_objects():
if decendant.INHERIT_PERMISSIONS:
decendant.group_read = self.group_read
decendant.group_write = self.group_write
decendant.other_read = self.other_read
decendant.other_write = self.other_write
def set_group_recursive(self):
for decendant in self.decendant_objects():
if decendant.INHERIT_PERMISSIONS:
decendant.group = self.group
def set_owner_recursive(self):
for decendant in self.decendant_objects():
if decendant.INHERIT_PERMISSIONS:
decendant.owner = self.owner
def add_revision(self, new_revision, save=True):
"""
......@@ -49,6 +83,7 @@ class Article(models.Model):
assert self.id or save, ('Article.add_revision: Sorry, you cannot add a'
'revision to an article that has not been saved '
'without using save=True')
if not self.id: self.save()
revisions = self.articlerevision_set.all()
try:
new_revision.revision_number = revisions.latest().revision_number + 1
......@@ -61,25 +96,40 @@ class Article(models.Model):
def add_object_relation(self, obj):
content_type = ContentType.objects.get_for_model(obj)
rel = ObjectForArticle.objects.get_or_create(article=self,
has_parent_field = hasattr(obj, 'parent')
rel = ArticleForObject.objects.get_or_create(article=self,
content_type=content_type,
object_pk=obj.pk,)
object_pk=obj.pk,
has_parent_method=has_parent_field)
return rel
@classmethod
def get_for_object(cls, obj):
return ArticleForObject.objects.get(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)).article
def __unicode__(self):
return self.title
class Meta:
app_label = settings.APP_LABEL
class ObjectForArticle(models.Model):
class ArticleForObject(models.Model):
article = models.ForeignKey('Article')
# Same as django.contrib.comments
content_type = models.ForeignKey(ContentType,
verbose_name=_('content type'),
related_name="content_type_set_for_%(class)s")
object_pk = models.TextField(_('object ID'))
content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
object_id = models.PositiveIntegerField(_('object ID'))
content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_id")
has_parent_method = models.BooleanField(default=False, editable=False)
class Meta:
app_label = settings.APP_LABEL
verbose_name = _(u'Article for object')
verbose_name_plural = _(u'Articles for object')
# Do not allow several objects
unique_together = ('content_type', 'object_id')
class ArticleRevision(models.Model):
......
# -*- coding: utf-8 -*-
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ugettext
from django.contrib.sites.models import Site
from mptt.models import MPTTModel
from mptt.fields import TreeForeignKey
......@@ -8,15 +8,22 @@ from mptt.fields import TreeForeignKey
from wiki.core.exceptions import NoRootURL, MultipleRootURLs
from wiki.conf import settings
from article import Article
from wiki.models.article import ArticleRevision, ArticleForObject
from django.contrib.contenttypes import generic
from django.core.exceptions import ValidationError
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.
"""
article = models.ForeignKey('wiki.Article',
verbose_name=_(u'article'),
help_text=_(u'Article to be displayed for this path'))
slug = models.SlugField(verbose_name=_(u'slug'))
# Tells django-wiki that permissions from a URLPath object's article
# should be inherited to children's articles
INHERIT_PERMISSIONS = True
articles = generic.GenericRelation(ArticleForObject)
slug = models.SlugField(verbose_name=_(u'slug'), null=True, blank=True)
site = models.ForeignKey(Site)
parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
......@@ -24,10 +31,11 @@ class URLPath(MPTTModel):
"/".join([obj.slug for obj in self.get_ancestors(include_self=True)])
class MPTTMeta:
order_insertion_by = ['slug']
pass
def __unicode__(self):
return self.path
path = self.get_path()
return path if path else ugettext(u"(root)")
def save(self, *args, **kwargs):
super(URLPath, self).save(*args, **kwargs)
......@@ -37,7 +45,17 @@ class URLPath(MPTTModel):
verbose_name_plural = _(u'URL paths')
unique_together = ('site', 'parent', 'slug')
app_label = settings.APP_LABEL
def clean(self, *args, **kwargs):
if self.slug and not self.parent:
raise ValidationError(_(u'Sorry but you cannot have a root article with a slug.'))
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):
raise ValidationError(_(u'There is already a root node on %s') % self.site)
super(URLPath, self).clean(*args, **kwargs)
@classmethod
def get_by_path(cls, path):
"""
......@@ -45,28 +63,44 @@ class URLPath(MPTTModel):
Accepts paths both starting with and without '/'
"""
site = Site.objects.get_current()
paths = cls.objects.filter(site=site)
root_nodes = paths.filter(parent=None)
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:
no_paths = root_nodes.count()
if no_paths == 0:
raise NoRootURL
if no_paths > 1:
raise MultipleRootURLs
return root_nodes[0]
slugs = path.split('/')
level = 1
parent = root_nodes[0]
for slug in slugs:
if settings.URL_CASE_SENSITIVE:
paths = paths.filter(parent__slug=slug)
parent = parent.get_children.get(slug=slug)
else:
paths = paths.filter(parent__slug__iexact=slug)
parent = parent.get_children.get(slug__iexact=slug)
level += 1
if paths.count() == 0:
raise cls.DoesNotExist
return paths[0]
\ No newline at end of file
return parent
@classmethod
def create_root(cls, site=None):
if not site: site = Site.objects.get_current()
if not cls.objects.root_nodes().filter(site=site):
# (get_or_create does not work for MPTT models??)
root = cls.objects.create(site=site)
article = Article()
article.add_revision(ArticleRevision(), save=True)
article.add_object_relation(root)
@property
def article(self):
try:
return self.articles.all()[0]
except IndexError:
return None
\ No newline at end of file
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