Python3 and Django 1.11 upgrade (#40)
* starting py3 upgrade * b64 not base64 * more progress, some more future things, and encoding * tox * update circle.yml * that version doesnt exist yet, oops * install? * sigh * Docker me? * update readme, requirements * fix for python3 * python35 * morgan remediations * does this work? * how about this? * one more time * no blank lines? * revert a bit * add future imports
This commit is contained in:
parent
fc974242ba
commit
4b7738c0f2
15
README.md
15
README.md
@ -7,6 +7,18 @@ Easy to use for testing remote storages when you're in a transition stage betwee
|
|||||||
|
|
||||||
Intended to be used in tests, never in production.
|
Intended to be used in tests, never in production.
|
||||||
|
|
||||||
|
## TESTING
|
||||||
|
|
||||||
|
I use Pyenv and Tox to support Python2.7.13 and Python3.6.3
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
pyenv install 2.7.13 3.5.4 3.6.3
|
||||||
|
tox
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can run individually with your shell python using `python setup.py test`
|
||||||
|
|
||||||
## INSTALLATION
|
## INSTALLATION
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -42,7 +54,7 @@ class SomeClass(models.Model):
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Test that this works on a fake model, not just the storage file.
|
- Test that this works on a fake model, not just the storage file.
|
||||||
- Different django and different python versions.
|
- Different django versions.
|
||||||
|
|
||||||
## Signing Key
|
## Signing Key
|
||||||
You can find my signing key at [TyrelSouza.com](https://tyrelsouza.com/koken/?/pages/pgp-keys/)
|
You can find my signing key at [TyrelSouza.com](https://tyrelsouza.com/koken/?/pages/pgp-keys/)
|
||||||
@ -51,6 +63,7 @@ I will sign everything with 0x769A1BC78A2DDEE2
|
|||||||
|
|
||||||
## CHANGELOG
|
## CHANGELOG
|
||||||
|
|
||||||
|
- 2018-02-01 [Tyrel Souza] Bump versions to django 1.11 and testing with Python3
|
||||||
- 2017-05-10 [Pamela McA'Nulty] Have save overwrite existing files
|
- 2017-05-10 [Pamela McA'Nulty] Have save overwrite existing files
|
||||||
- 2017-02-06 [Tyrel Souza] Set primary key to `id` not `name`, this involves a lot of migrations, so I've kept them in multiple files
|
- 2017-02-06 [Tyrel Souza] Set primary key to `id` not `name`, this involves a lot of migrations, so I've kept them in multiple files
|
||||||
- 2017-01-27 [Tyrel Souza] Get rid of filehash
|
- 2017-01-27 [Tyrel Souza] Get rid of filehash
|
||||||
|
11
circle.yml
11
circle.yml
@ -1,3 +1,14 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
working_directory: ~/django-dbfilestorage
|
||||||
|
docker:
|
||||||
|
- image: themattrix/tox
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: tox
|
||||||
|
|
||||||
test:
|
test:
|
||||||
post:
|
post:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import DBFile
|
from .models import DBFile
|
||||||
|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from future.utils import python_2_unicode_compatible
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class DBFile(models.Model):
|
class DBFile(models.Model):
|
||||||
""" Model to store and access uploaded files """
|
""" Model to store and access uploaded files """
|
||||||
|
|
||||||
@ -11,7 +15,7 @@ class DBFile(models.Model):
|
|||||||
b64 = models.TextField()
|
b64 = models.TextField()
|
||||||
mtime = models.DateTimeField(auto_now=True)
|
mtime = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u"{name} <{content_type}>".format(
|
return u"{name} <{content_type}>".format(
|
||||||
name=self.name, content_type=self.content_type)
|
name=self.name, content_type=self.content_type)
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -6,7 +9,7 @@ from django.db.transaction import atomic
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import Storage
|
from django.core.files.storage import Storage
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
|
||||||
from .models import DBFile
|
from .models import DBFile
|
||||||
|
|
||||||
@ -29,7 +32,7 @@ class DBFileStorage(Storage):
|
|||||||
|
|
||||||
def _open(self, name, mode='rb'):
|
def _open(self, name, mode='rb'):
|
||||||
the_file = _get_object(name)
|
the_file = _get_object(name)
|
||||||
return ContentFile(the_file.b64.decode('base64'))
|
return ContentFile(base64.b64decode(the_file.b64))
|
||||||
|
|
||||||
@atomic
|
@atomic
|
||||||
def _save(self, name, content, max_length=None):
|
def _save(self, name, content, max_length=None):
|
||||||
@ -43,8 +46,13 @@ class DBFileStorage(Storage):
|
|||||||
Then it checks if the file exists and if it doesn't, it will create
|
Then it checks if the file exists and if it doesn't, it will create
|
||||||
the entry in the database.
|
the entry in the database.
|
||||||
|
|
||||||
|
:param name: file name to save
|
||||||
|
:param content: Content object, where content.file is bytes
|
||||||
|
|
||||||
:return str: the name(md5) to look up the file by.
|
:return str: the name(md5) to look up the file by.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO: Make this conditional better
|
||||||
if hasattr(content.file, "read"):
|
if hasattr(content.file, "read"):
|
||||||
read_data = content.file.read()
|
read_data = content.file.read()
|
||||||
if not read_data:
|
if not read_data:
|
||||||
@ -52,8 +60,9 @@ class DBFileStorage(Storage):
|
|||||||
content.file.seek(0)
|
content.file.seek(0)
|
||||||
read_data = content.file.read()
|
read_data = content.file.read()
|
||||||
else:
|
else:
|
||||||
read_data = content.file.encode('utf8')
|
read_data = content.file
|
||||||
b64 = read_data.encode('base64')
|
|
||||||
|
b64 = base64.b64encode(read_data)
|
||||||
|
|
||||||
# USE mimetypes.guess_type as an attempt at getting the content type.
|
# USE mimetypes.guess_type as an attempt at getting the content type.
|
||||||
ct = mimetypes.guess_type(name)[0]
|
ct = mimetypes.guess_type(name)[0]
|
||||||
@ -91,8 +100,8 @@ class DBFileStorage(Storage):
|
|||||||
def url(self, name):
|
def url(self, name):
|
||||||
dbf = _get_object(name)
|
dbf = _get_object(name)
|
||||||
if dbf:
|
if dbf:
|
||||||
return reverse('dbstorage_file', args=(dbf.name,))
|
return reverse_lazy('dbstorage_file', args=(dbf.name,))
|
||||||
return reverse('dbstorage_file', args=(name,))
|
return reverse_lazy('dbstorage_file', args=(name,))
|
||||||
|
|
||||||
def modified_time(self, name):
|
def modified_time(self, name):
|
||||||
dbf = _get_object(name)
|
dbf = _get_object(name)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
import views
|
from dbfilestorage import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^(?P<name>.*)$', views.show_file, name="dbstorage_file"),
|
url(r'^(?P<name>.*)$', views.show_file, name="dbstorage_file"),
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
from django.http import HttpResponse, Http404
|
from django.http import HttpResponse, Http404
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@ -15,7 +19,7 @@ def show_file(request, name):
|
|||||||
"""
|
"""
|
||||||
dbf = get_object_or_404(DBFile, name=name)
|
dbf = get_object_or_404(DBFile, name=name)
|
||||||
response = HttpResponse(
|
response = HttpResponse(
|
||||||
dbf.b64.decode('base64'),
|
base64.b64decode(dbf.b64),
|
||||||
content_type=dbf.content_type)
|
content_type=dbf.content_type)
|
||||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(
|
||||||
dbf.name)
|
dbf.name)
|
||||||
|
4
requirements-dev.txt
Normal file
4
requirements-dev.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Sphinx==1.5
|
||||||
|
coverage==4.2
|
||||||
|
future
|
||||||
|
tox
|
@ -1,3 +1 @@
|
|||||||
Django==1.10.4
|
Django==1.11.10
|
||||||
Sphinx==1.5
|
|
||||||
coverage==4.2
|
|
||||||
|
6
setup.py
6
setup.py
@ -21,10 +21,11 @@ class CleanCommand(Command):
|
|||||||
os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info')
|
os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info')
|
||||||
os.system('rm -vrf htmlcov .coverage')
|
os.system('rm -vrf htmlcov .coverage')
|
||||||
os.system('rm -vrf .DS_Store')
|
os.system('rm -vrf .DS_Store')
|
||||||
|
os.system('rm -vrf .tox')
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="django-dbfilestorage",
|
name="django-dbfilestorage",
|
||||||
version="0.9.2",
|
version="0.9.3",
|
||||||
description="Database backed file storage for testing.",
|
description="Database backed file storage for testing.",
|
||||||
long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.",
|
long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.",
|
||||||
author="Tyrel Souza",
|
author="Tyrel Souza",
|
||||||
@ -38,11 +39,12 @@ setup(
|
|||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"Django>=1.8.0",
|
"Django==1.11.10",
|
||||||
],
|
],
|
||||||
tests_require=[
|
tests_require=[
|
||||||
"nose",
|
"nose",
|
||||||
"coverage",
|
"coverage",
|
||||||
|
"future",
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
test_suite="tests.runtests.start",
|
test_suite="tests.runtests.start",
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import base64
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dbfilestorage.models import DBFile
|
from dbfilestorage.models import DBFile
|
||||||
@ -11,6 +13,7 @@ from django.test import TestCase, Client
|
|||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__))
|
PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__))
|
||||||
DEFAULT_FILE_STORAGE = "dbfilestorage.storage.DBFileStorage"
|
DEFAULT_FILE_STORAGE = "dbfilestorage.storage.DBFileStorage"
|
||||||
|
|
||||||
@ -36,10 +39,10 @@ class DBFileTest(TestCase):
|
|||||||
|
|
||||||
def test_content_file(self):
|
def test_content_file(self):
|
||||||
""" Test that this code works with ContentFile as well """
|
""" Test that this code works with ContentFile as well """
|
||||||
content_file = ContentFile(u"ƊBStørage")
|
content_file = ContentFile(b"\\u018aBSt\\xf8rage")
|
||||||
default_storage.save("unicode", content_file)
|
default_storage.save("unicode", content_file)
|
||||||
unicode_file = DBFile.objects.get(name="unicode")
|
unicode_file = DBFile.objects.get(name="unicode")
|
||||||
self.assertEqual(unicode(unicode_file),
|
self.assertEqual(str(unicode_file),
|
||||||
"unicode <application/octet-stream>")
|
"unicode <application/octet-stream>")
|
||||||
|
|
||||||
def test_no_duplicate_upload(self):
|
def test_no_duplicate_upload(self):
|
||||||
@ -52,7 +55,7 @@ class DBFileTest(TestCase):
|
|||||||
""" Test that the DB entry matches what is expected from the file """
|
""" Test that the DB entry matches what is expected from the file """
|
||||||
with open(self.filepath, 'rb') as f:
|
with open(self.filepath, 'rb') as f:
|
||||||
dbf = DBFile.objects.get(name=self.filepath)
|
dbf = DBFile.objects.get(name=self.filepath)
|
||||||
self.assertEqual(dbf.b64.decode("base64"), f.read())
|
self.assertEqual(base64.b64decode(dbf.b64), f.read())
|
||||||
self.assertEqual(dbf.content_type, 'image/jpeg')
|
self.assertEqual(dbf.content_type, 'image/jpeg')
|
||||||
|
|
||||||
def test_open(self):
|
def test_open(self):
|
||||||
@ -62,7 +65,7 @@ class DBFileTest(TestCase):
|
|||||||
self.assertEqual(dbf.read(), f.read())
|
self.assertEqual(dbf.read(), f.read())
|
||||||
|
|
||||||
def test_exists(self):
|
def test_exists(self):
|
||||||
""" Test that the storage mechanism can check existance """
|
""" Test that the storage mechanism can check existence """
|
||||||
self.assertTrue(default_storage.exists(self.filepath))
|
self.assertTrue(default_storage.exists(self.filepath))
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
@ -81,8 +84,8 @@ class DBFileTest(TestCase):
|
|||||||
self.assertGreater(size, 0)
|
self.assertGreater(size, 0)
|
||||||
|
|
||||||
def test_raw_save(self):
|
def test_raw_save(self):
|
||||||
CONTENT_DATA_1 = u"Here's some stuff! ƊBStørage - ONE"
|
CONTENT_DATA_1 = "Here's some stuff! ƊBStørage - ONE"
|
||||||
CONTENT_DATA_2 = u"Here's some stuff! ƊBStørage - TWO"
|
CONTENT_DATA_2 = "Here's some stuff! ƊBStørage - TWO"
|
||||||
FILE_NAME = "saveable.txt"
|
FILE_NAME = "saveable.txt"
|
||||||
self.assertFalse(DBFile.objects.filter(name=FILE_NAME).exists())
|
self.assertFalse(DBFile.objects.filter(name=FILE_NAME).exists())
|
||||||
|
|
||||||
@ -142,9 +145,9 @@ class DBFileTest(TestCase):
|
|||||||
def test_listdir(self):
|
def test_listdir(self):
|
||||||
""" Make sure listdir works, and only returns things under 'dirname' """
|
""" Make sure listdir works, and only returns things under 'dirname' """
|
||||||
names = [
|
names = [
|
||||||
u'dirname/kris.jpg',
|
'dirname/kris.jpg',
|
||||||
u'dirname/kris2.jpg',
|
'dirname/kris2.jpg',
|
||||||
u'dirname/kris3.jpg']
|
'dirname/kris3.jpg']
|
||||||
|
|
||||||
for name in names:
|
for name in names:
|
||||||
self._upload(name=name)
|
self._upload(name=name)
|
||||||
|
@ -1,18 +1,5 @@
|
|||||||
"""tests URL Configuration
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.conf.urls import url, include
|
|
||||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
"""
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
WSGI config for tests project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user