Commit 7b6078b8 by Bridger Maxwell

Merge branch 'master' of github.com:benjaoming/django-wiki

parents 7ec4943b ab7dc9a0
...@@ -115,6 +115,7 @@ So far the dependencies are: ...@@ -115,6 +115,7 @@ So far the dependencies are:
* [Markdown>=2.2.0](https://github.com/waylan/Python-Markdown) * [Markdown>=2.2.0](https://github.com/waylan/Python-Markdown)
* [django-mptt>=0.5](https://github.com/django-mptt/django-mptt) * [django-mptt>=0.5](https://github.com/django-mptt/django-mptt)
* [django-sekizai](https://github.com/ojii/django-sekizai/) * [django-sekizai](https://github.com/ojii/django-sekizai/)
* [sorl-thumbnail](https://github.com/sorl/sorl-thumbnail)
Development Development
------------ ------------
......
Not implemented - will be ASAP Not implemented - will be ASAP
============================== ==============================
* Permission system in settings tab **Done** * Permission system in settings tab **Almost done** (option to apply changes recursively and to change owner if user has a grant permission)
* Notification system **Almost done** (email notifications) * Notification system **Almost done** (email notifications)
* Simple user account handling: login/register etc. **Done** * Image plugin **In progress**
* Circuit Editor plugin
* Custom storage engine for attachments
* Implement notifications, revision log messages and user messages thoroughly * Implement notifications, revision log messages and user messages thoroughly
* Attachment plugin **Done**
* Image plugin
* Example plugin * Example plugin
* Bot editing detection. Don't let anyone edit more than once every other minute. * Spam protection / Bot editing detection. Don't let anyone edit more than once every other minute.
* Article deletion **Done**
* Key-value meta data * Key-value meta data
* Index views for urlpaths * Index views for urlpaths
* Searching * Searching
* South migrations **Done** * Finish all class-based views
* View source for read-only articles + locked status * View source for read-only articles + locked status
* Global moderator permission **Almost done** (need to add grant form for users with *grant* permissions)
* Are you sure you wanna leave this page? * Are you sure you wanna leave this page?
* Customize bootstrap to look a bit less like a default installation and add styles for the wiki articles
* Embeddable article template tag
* Expand prepopulated database
* "Fix Wiki URL bug in the footnotes plugin for python-markdown" ?
* Special view for deleted articles w/ restore button **Done** * Special view for deleted articles w/ restore button **Done**
* Article deletion **Done**
* Attachment plugin **Done**
* Simple user account handling: login/register etc. **Done**
* South migrations **Done**
Ideas Ideas
===== =====
......
...@@ -3,3 +3,4 @@ South<0.8 ...@@ -3,3 +3,4 @@ South<0.8
Markdown<2.3.0 Markdown<2.3.0
django-sekizai<0.7 django-sekizai<0.7
django-mptt>=0.5 django-mptt>=0.5
sorl-thumbnail
\ No newline at end of file
...@@ -6,6 +6,17 @@ that you have checked out the root of the Git repository. ...@@ -6,6 +6,17 @@ that you have checked out the root of the Git repository.
It comes with a prepopulated SQLite database. It comes with a prepopulated SQLite database.
Setup
-----
You should link your wiki and django_notify folders like so
ln -s ../wiki .
ln -s ../django_notify .
Login
-----
Django admin: Django admin:
Username: admin Username: admin
......
...@@ -104,6 +104,7 @@ INSTALLED_APPS = ( ...@@ -104,6 +104,7 @@ INSTALLED_APPS = (
'south', 'south',
'sekizai', 'sekizai',
'django_notify', 'django_notify',
'sorl.thumbnail',
'wiki', 'wiki',
'wiki.plugins.images', 'wiki.plugins.images',
'wiki.plugins.attachments', 'wiki.plugins.attachments',
......
...@@ -3,10 +3,12 @@ from django.conf import settings as django_settings ...@@ -3,10 +3,12 @@ from django.conf import settings as django_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import redirect, get_object_or_404, render_to_response from django.shortcuts import redirect, get_object_or_404, render_to_response
from django.template.context import RequestContext from django.template.context import RequestContext
from django.http import HttpResponse, HttpResponseNotFound from django.http import HttpResponse, HttpResponseNotFound,\
HttpResponseForbidden
from django.utils import simplejson as json from django.utils import simplejson as json
from wiki.core.exceptions import NoRootURL from wiki.core.exceptions import NoRootURL
from django.template.loader import render_to_string
def json_view(func): def json_view(func):
def wrap(request, *args, **kwargs): def wrap(request, *args, **kwargs):
...@@ -47,16 +49,15 @@ def get_article(func=None, can_read=True, can_write=False, deleted_contents=Fals ...@@ -47,16 +49,15 @@ def get_article(func=None, can_read=True, can_write=False, deleted_contents=Fals
articles = models.Article.objects articles = models.Article.objects
# TODO: Is this the way to do it? # TODO: Is this the way to do it?
articles = articles.select_related() # https://docs.djangoproject.com/en/1.4/ref/models/querysets/#django.db.models.query.QuerySet.prefetch_related
# This is not the way to go... optimize below statements to behave
# according to normal prefetching.
articles = articles.prefetch_related()
urlpath = None urlpath = None
if article_id:
article = get_object_or_404(articles, id=article_id) # fetch by urlpath.path
try: if not path is None:
urlpath = models.URLPath.objects.get(articles__article=article)
except models.URLPath.DoesNotExist, models.URLPath.MultipleObjectsReturned:
urlpath = None
else:
try: try:
urlpath = models.URLPath.get_by_path(path, select_related=True) urlpath = models.URLPath.get_by_path(path, select_related=True)
except NoRootURL: except NoRootURL:
...@@ -78,19 +79,33 @@ def get_article(func=None, can_read=True, can_write=False, deleted_contents=Fals ...@@ -78,19 +79,33 @@ def get_article(func=None, can_read=True, can_write=False, deleted_contents=Fals
urlpath.delete() urlpath.delete()
return redirect(return_url) return redirect(return_url)
# fetch by article.id
elif article_id:
article = get_object_or_404(articles, id=article_id)
try:
urlpath = models.URLPath.objects.get(articles__article=article)
except models.URLPath.DoesNotExist, models.URLPath.MultipleObjectsReturned:
urlpath = None
else:
# TODO: Return something??
raise TypeError('You should specify either article_id or path')
if can_read and not article.can_read(request.user): if can_read and not article.can_read(request.user):
if request.user.is_anonymous(): if request.user.is_anonymous():
return redirect(django_settings.LOGIN_URL) return redirect(django_settings.LOGIN_URL)
else: else:
c = RequestContext(request, {'urlpath' : urlpath}) c = RequestContext(request, {'urlpath' : urlpath})
return render_to_response("wiki/permission_denied.html", context_instance=c) return HttpResponseForbidden(render_to_string("wiki/permission_denied.html", c))
if can_write and not article.can_write(request.user): if can_write and not article.can_write(request.user):
if request.user.is_anonymous(): if request.user.is_anonymous():
return redirect(django_settings.LOGIN_URL) return redirect(django_settings.LOGIN_URL)
else: else:
c = RequestContext(request, {'urlpath' : urlpath}) c = RequestContext(request, {'urlpath' : urlpath})
return render_to_response("wiki/permission_denied.html", context_instance=c) return HttpResponseForbidden(render_to_string("wiki/permission_denied.html", c))
# If the article has been deleted, show a special page. # If the article has been deleted, show a special page.
if not deleted_contents and article.current_revision and article.current_revision.deleted: if not deleted_contents and article.current_revision and article.current_revision.deleted:
......
...@@ -4,7 +4,7 @@ from django.contrib.sites.models import Site ...@@ -4,7 +4,7 @@ from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete, post_save
from django.utils.translation import ugettext_lazy as _, ugettext from django.utils.translation import ugettext_lazy as _, ugettext
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
...@@ -13,6 +13,7 @@ from mptt.models import MPTTModel ...@@ -13,6 +13,7 @@ from mptt.models import MPTTModel
from wiki.conf import settings from wiki.conf import settings
from wiki.core.exceptions import NoRootURL, MultipleRootURLs from wiki.core.exceptions import NoRootURL, MultipleRootURLs
from wiki.models.article import ArticleRevision, ArticleForObject, Article from wiki.models.article import ArticleRevision, ArticleForObject, Article
from django.contrib.contenttypes.models import ContentType
class URLPath(MPTTModel): class URLPath(MPTTModel):
""" """
...@@ -25,6 +26,11 @@ class URLPath(MPTTModel): ...@@ -25,6 +26,11 @@ class URLPath(MPTTModel):
INHERIT_PERMISSIONS = True INHERIT_PERMISSIONS = True
articles = generic.GenericRelation(ArticleForObject) articles = generic.GenericRelation(ArticleForObject)
# Do NOT modify this field - it is updated with signals whenever ArticleForObject is changed.
article = models.ForeignKey(Article, on_delete=models.CASCADE, editable=False,
verbose_name=_(u'Cache lookup value for articles'))
slug = models.SlugField(verbose_name=_(u'slug'), null=True, blank=True) slug = models.SlugField(verbose_name=_(u'slug'), null=True, blank=True)
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')
...@@ -85,6 +91,11 @@ class URLPath(MPTTModel): ...@@ -85,6 +91,11 @@ 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 '/'
""" """
# TODO: Save paths directly in the model for constant time lookups?
# Or: Save the parents in a lazy property because the parents are
# always fetched anyways so it's fine to fetch them here.
path = path.lstrip("/") path = path.lstrip("/")
path = path.rstrip("/") path = path.rstrip("/")
...@@ -127,25 +138,28 @@ class URLPath(MPTTModel): ...@@ -127,25 +138,28 @@ class URLPath(MPTTModel):
"""Utility function: """Utility function:
Create a new urlpath with an article and a new revision for the article""" Create a new urlpath with an article and a new revision for the article"""
if not site: site = Site.objects.get_current() if not site: site = Site.objects.get_current()
newpath = cls.objects.create(site=site, parent=parent, slug=slug)
article = Article(**article_kwargs) article = Article(**article_kwargs)
article.add_revision(ArticleRevision(title=title, **kwargs), article.add_revision(ArticleRevision(title=title, **kwargs),
save=True) save=True)
article.save()
newpath = cls.objects.create(site=site, parent=parent, slug=slug, article=article)
article.add_object_relation(newpath) article.add_object_relation(newpath)
return newpath return newpath
@property
def article(self):
try:
return self.articles.all()[0].article
except IndexError:
return None
###################################################### ######################################################
# SIGNAL HANDLERS # SIGNAL HANDLERS
###################################################### ######################################################
# Just get this once
urlpath_content_type = ContentType.objects.get_for_model(URLPath)
def on_article_relation_save(instance, *args, **kwargs):
if instance.content_type == urlpath_content_type:
URLPath.objects.filter(id=instance.object_id).update(article=instance.article)
post_save.connect(on_article_relation_save, ArticleForObject)
def on_article_delete(instance, *args, **kwargs): def on_article_delete(instance, *args, **kwargs):
# If an article is deleted, then throw out its URLPaths # If an article is deleted, then throw out its URLPaths
# But move all descendants to a lost-and-found node. # But move all descendants to a lost-and-found node.
...@@ -174,6 +188,7 @@ def on_article_delete(instance, *args, **kwargs): ...@@ -174,6 +188,7 @@ def on_article_delete(instance, *args, **kwargs):
for child in urlpath.get_children(): for child in urlpath.get_children():
child.move_to(lost_and_found) child.move_to(lost_and_found)
# ...and finally delete the path itself # ...and finally delete the path itself
# TODO: This should be unnecessary because of URLPath.article(...ondelete=models.CASCADE)
urlpath.delete() urlpath.delete()
pre_delete.connect(on_article_delete, Article) pre_delete.connect(on_article_delete, Article)
...@@ -61,7 +61,6 @@ class Migration(SchemaMigration): ...@@ -61,7 +61,6 @@ class Migration(SchemaMigration):
}, },
'images.image': { 'images.image': {
'Meta': {'object_name': 'Image', '_ormbases': ['wiki.RevisionPlugin']}, 'Meta': {'object_name': 'Image', '_ormbases': ['wiki.RevisionPlugin']},
'caption': ('django.db.models.fields.CharField', [], {'max_length': '2056', 'null': 'True', 'blank': 'True'}),
'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'revisionplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['wiki.RevisionPlugin']", 'unique': 'True', 'primary_key': 'True'}) 'revisionplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['wiki.RevisionPlugin']", 'unique': 'True', 'primary_key': 'True'})
}, },
......
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -5,9 +7,13 @@ import settings ...@@ -5,9 +7,13 @@ import settings
from wiki.models.pluginbase import RevisionPlugin from wiki.models.pluginbase import RevisionPlugin
if not "sorl.thumbnail" in django_settings.INSTALLED_APPS:
raise ImproperlyConfigured('wiki.plugins.images: needs sorl.thumbnail in INSTALLED_APPS')
class Image(RevisionPlugin): class Image(RevisionPlugin):
image = models.ImageField(upload_to=settings.IMAGE_PATH) image = models.ImageField(upload_to=settings.IMAGE_PATH,
max_length=2000)
def get_filename(self): def get_filename(self):
if self.image: if self.image:
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
{% endfor %} {% endfor %}
</table> </table>
{% if article|can_write:user %}
<hr /> <hr />
<h4>{% trans "Add new image" %}</h4> <h4>{% trans "Add new image" %}</h4>
...@@ -55,3 +56,5 @@ ...@@ -55,3 +56,5 @@
{% trans "Add image" %} {% trans "Add image" %}
</button> </button>
</p> </p>
{% endif %}
from wiki.views.mixins import ArticleMixin
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from wiki.decorators import get_article from wiki.decorators import get_article
from wiki.views.mixins import ArticleMixin
class ImageView(ArticleMixin, TemplateView): class ImageView(ArticleMixin, TemplateView):
......
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