diff --git a/CHANGELOG b/CHANGELOG index 8787808..147b302 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,8 @@ Changelog Bugfixes and documentation improvements. +- Bug #26 - Prevented computation of virtual file's size, unless the file + wrapper implements was_modified_since() method. - Bug #34 - Improved support of files that do not implement modification time. - Bug #35 - Fixed README conversion from reStructuredText to HTML (PyPI). diff --git a/django_downloadview/views.py b/django_downloadview/views.py index ab1a0e8..8eee5d9 100644 --- a/django_downloadview/views.py +++ b/django_downloadview/views.py @@ -173,6 +173,27 @@ class VirtualDownloadView(BaseDownloadView): """Return wrapper.""" raise NotImplementedError() + def was_modified_since(self, file_instance, since): + """Delegate to file wrapper's was_modified_since, or return True. + + This is the implementation of an edge case: when files are generated + on the fly, we cannot guess whether they have been modified or not. + If the file wrapper implements ``was_modified_since()`` method, then we + trust it. Otherwise it is safer to suppose that the file has been + modified. + + This behaviour prevents file size to be computed on the Django side. + Because computing file size means iterating over all the file contents, + and we want to avoid that whenever possible. As an example, it could + reduce all the benefits of working with dynamic file generators... + which is a major feature of virtual files. + + """ + try: + return file_instance.was_modified_since(since) + except (AttributeError, NotImplementedError): + return True + class HTTPDownloadView(BaseDownloadView): """Proxy files that live on remote servers.""" diff --git a/tests/views.py b/tests/views.py index 77ebca4..a98e2a9 100644 --- a/tests/views.py +++ b/tests/views.py @@ -48,3 +48,36 @@ class DownloadMixinTestCase(unittest.TestCase): mixin = views.DownloadMixin() since = mock.sentinel.since self.assertTrue(mixin.was_modified_since(file_wrapper, since)) + + +class VirtualDownloadViewTestCase(unittest.TestCase): + """Test suite around + :py:class:`django_downloadview.views.VirtualDownloadView`.""" + def test_was_modified_since_specific(self): + """VirtualDownloadView.was_modified_since() delegates to file wrapper. + + """ + file_wrapper = mock.Mock() + file_wrapper.was_modified_since = mock.Mock( + return_value=mock.sentinel.from_file_wrapper) + view = views.VirtualDownloadView() + since = mock.sentinel.since + return_value = view.was_modified_since(file_wrapper, since) + self.assertTrue(return_value is mock.sentinel.from_file_wrapper) + file_wrapper.was_modified_since.assert_called_once_with(since) + + def test_was_modified_since_not_implemented(self): + """VirtualDownloadView.was_modified_since() returns True if file + wrapper does not implement ``was_modified_since()``.""" + file_wrapper = mock.Mock() + file_wrapper.was_modified_since = mock.Mock(side_effect=AttributeError) + modified_time = mock.PropertyMock() + setattr(file_wrapper, 'modified_time', modified_time) + size = mock.PropertyMock() + setattr(file_wrapper, 'size', size) + view = views.VirtualDownloadView() + since = mock.sentinel.since + result = view.was_modified_since(file_wrapper, since) + self.assertTrue(result is True) + self.assertFalse(modified_time.called) + self.assertFalse(size.called)