M docs/release_notes.rst => docs/release_notes.rst +1 -1
@@ 32,7 32,7 @@ django-wiki 0.1.2 (unreleased)
------------------------------
* Remove unwanted items from default menu when ``WIKI_ACCOUNT_HANDLING = False``. #545
-
+ * Fix broken soft-deletion and restoring of images, and "set revision" functionality #533
django-wiki 0.1.1
-----------------
M wiki/models/article.py => wiki/models/article.py +16 -0
@@ 308,6 308,22 @@ class BaseRevisionMixin(models.Model):
elif settings.LOG_IPS_ANONYMOUS:
self.ip_address = request.META.get('REMOTE_ADDR', None)
+ def inherit_predecessor(self, predecessor):
+ """
+ This is a naive way of inheriting, assuming that ``predecessor`` is in
+ fact the predecessor and there hasn't been any intermediate changes!
+
+ :param: predecessor is an instance of whatever object for which
+ object.current_revision implements BaseRevisionMixin.
+ """
+ predecessor = predecessor.current_revision
+ self.previous_revision = predecessor
+ self.deleted = predecessor.deleted
+ self.locked = predecessor.locked
+ self.deleted = predecessor.deleted
+ self.locked = predecessor.locked
+ self.revision_number = predecessor.revision_number + 1
+
class Meta:
abstract = True
M wiki/plugins/images/markdown_extensions.py => wiki/plugins/images/markdown_extensions.py +0 -1
@@ 12,7 12,6 @@ IMAGE_RE = re.compile(
re.IGNORECASE)
-
class ImageExtension(markdown.Extension):
""" Images plugin markdown extension for django-wiki. """
M wiki/plugins/images/models.py => wiki/plugins/images/models.py +6 -1
@@ 91,8 91,13 @@ class ImageRevision(RevisionPluginRevision):
"""
Inherit certain properties from predecessor because it's very
convenient. Remember to always call this method before
- setting properties :)"""
+ setting properties :)
+
+ A revision may not have a predecessor if the property is unset, it may
+ be unset if it's the initial history entry.
+ """
predecessor = image.current_revision.imagerevision
+ super(ImageRevision, self).inherit_predecessor(image)
self.plugin = predecessor.plugin
self.deleted = predecessor.deleted
self.locked = predecessor.locked
M wiki/plugins/images/templates/wiki/plugins/images/index.html => wiki/plugins/images/templates/wiki/plugins/images/index.html +2 -2
@@ 36,7 36,7 @@
{% trans "Upload and publish new image" %}
</a> <br />
<a href="{% url 'wiki:images_restore' path=urlpath.path article_id=article.id image_id=image.id %}">
- <span class="fa ra-repeat"></span>
+ <span class="fa fa-refresh"></span>
{% trans "Restore image" %}
</a>
{% else %}
@@ 84,7 84,7 @@
<td>{{ old_revision.imagerevision.width }}x{{ old_revision.imagerevision.height }}</td>
<td>
{% if image|can_write:user and old_revision != image.current_revision %}
- <a href="#">
+ <a href="{% url 'wiki:images_set_revision' path=urlpath.path article_id=article.id image_id=image.id rev_id=old_revision.id %}">
<span class="fa fa-refresh"></span>
{% trans "Revert to this version" %}
</a>
M => +1 -1
@@ 27,7 27,7 @@ function insert_image(image_id) {
{% thumbnail revision.image "50x50" crop="center" as thumb %}
<tr>
<td style="white-space: nowrap;">
<p>{% trans "Image id" %}: {{ image.id }}</code></p>
<p>{% trans "Image id" %}: {{ image.id }}</p>
<p>
<a href="javascript:void(insert_image({{ image.id }}))"><span class="fa fa-edit"></span> {% trans "Insert" %}</a><br />
{% if image|can_write:user %}
A wiki/plugins/images/tests/__init__.py => wiki/plugins/images/tests/__init__.py +6 -0
@@ 0,0 1,6 @@
+from __future__ import absolute_import
+
+import django
+if django.VERSION < (1, 6):
+ # New style autodiscovery of tests doesn't work for Django < 1.6
+ from .test_views import * # noqa
A wiki/plugins/images/tests/test_views.py => wiki/plugins/images/tests/test_views.py +87 -0
@@ 0,0 1,87 @@
+from __future__ import print_function, unicode_literals
+
+import base64
+from io import BytesIO
+
+from django.core.files.uploadedfile import InMemoryUploadedFile
+from django.core.urlresolvers import reverse
+from wiki.core.plugins import registry as plugin_registry
+from wiki.tests.base import ArticleWebTestBase
+
+from .. import models
+from ..wiki_plugin import ImagePlugin
+
+
+class ImageTests(ArticleWebTestBase):
+
+ def setUp(self):
+ super(ImageTests, self).setUp()
+ self.article = self.root_article
+ # A black 1x1 gif
+ self.test_data = "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="
+
+ def _create_gif_filestream_from_base64(self, str_base64, **kwargs):
+ """
+ Helper function to create filestream for upload.
+
+ Parameters :
+ strData : str, test string data
+
+ Optional Arguments :
+ filename : str, Defaults to 'test.txt'
+ """
+ filename = kwargs.get('filename', 'test.gif')
+ data = base64.b64decode(str_base64)
+ filedata = BytesIO(data)
+ filestream = InMemoryUploadedFile(
+ filedata,
+ None,
+ filename,
+ 'image',
+ len(data),
+ None
+ )
+ return filestream
+
+ def _create_test_image(self):
+ # Get the form index
+ plugin_index = -1
+ for cnt, plugin_instance in enumerate(plugin_registry.get_sidebar()):
+ if isinstance(plugin_instance, ImagePlugin):
+ plugin_index = cnt
+ break
+ self.assertTrue(plugin_index >= 0, "Image plugin not activated")
+ base_edit_url = reverse('wiki:edit', kwargs={'path': ''})
+ url = base_edit_url + '?f=form{0:d}'.format(plugin_index)
+ filestream = self._create_gif_filestream_from_base64(self.test_data)
+ response = self.c.post(
+ url,
+ {
+ 'unsaved_article_title': self.article.current_revision.title,
+ 'unsaved_article_content': self.article.current_revision.content,
+ 'image': filestream,
+ 'images_save': '1',
+ },
+ )
+ self.assertRedirects(response, base_edit_url)
+
+ def test_index(self):
+ url = reverse('wiki:images_index', kwargs={'path': ''})
+ response = self.c.get(url,)
+ self.assertContains(response, 'Images')
+
+ def test_upload(self):
+ """
+ Tests that simple file upload uploads correctly
+ Uploading a file should preserve the original filename.
+ Uploading should not modify file in any way.
+ """
+ self._create_test_image()
+ # Check the object was created.
+ image = models.Image.objects.get()
+ image_revision = image.current_revision.imagerevision
+ self.assertEqual(image_revision.get_filename(), 'test.gif')
+ self.assertEqual(
+ image_revision.image.file.read(),
+ base64.b64decode(self.test_data)
+ )
M wiki/plugins/images/views.py => wiki/plugins/images/views.py +13 -0
@@ 1,5 1,7 @@
from __future__ import absolute_import, unicode_literals
+import logging
+
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, redirect
@@ 10,9 12,12 @@ from django.views.generic.edit import FormView
from django.views.generic.list import ListView
from wiki.conf import settings as wiki_settings
from wiki.decorators import get_article
+from wiki.models.pluginbase import RevisionPluginRevision
from wiki.plugins.images import forms, models
from wiki.views.mixins import ArticleMixin
+logger = logging.getLogger(__name__)
+
class ImageView(ArticleMixin, ListView):
@@ 60,9 65,17 @@ class DeleteView(ArticleMixin, RedirectView):
def get_redirect_url(self, **kwargs):
+ if not self.image.current_revision:
+ logger.critical('Encountered an image without current revision set, ID: {}'.format(self.image.id))
+ latest_revision = RevisionPluginRevision.objects.filter(
+ plugin=self.image
+ ).latest('pk')
+ self.image.current_revision = latest_revision
+
new_revision = models.ImageRevision()
new_revision.inherit_predecessor(self.image)
new_revision.set_from_request(self.request)
+ new_revision.revision_number = RevisionPluginRevision.objects.filter(plugin=self.image).count()
new_revision.deleted = not self.restore
new_revision.save()
self.image.current_revision = new_revision
M wiki/plugins/images/wiki_plugin.py => wiki/plugins/images/wiki_plugin.py +1 -1
@@ 60,7 60,7 @@ class ImagePlugin(BasePlugin):
name='images_purge'),
url('^(?P<image_id>\d+)/revision/change/(?P<rev_id>\d+)/$',
views.RevisionChangeView.as_view(),
- name='images_restore'),
+ name='images_set_revision'),
url('^(?P<image_id>\d+)/revision/add/$',
views.RevisionAddView.as_view(),
name='images_add_revision'),