Initial commit after moving from Gitlab

This commit is contained in:
Tyrel Souza 2016-12-07 22:50:48 -05:00
commit 592373d1d3
No known key found for this signature in database
GPG Key ID: 2EECB5087209E6A5
26 changed files with 816 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
__pycache__/
*.py[cod]
build/
dist/
sdist/
.eggs/
*.egg-info/
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
*.db
# Sphinx documentation
docs/_build/

14
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,14 @@
image: python:2.7
before_script:
#- apt-get update -qq && apt-get install -y -qq python python-pip
- python --version
- pip install -r requirements.txt
types:
- test
job_test:
type: test
script:
- python setup.py test

22
LICENSE.txt Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2016 Tyrel Souza
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
README.rst Normal file
View File

@ -0,0 +1,21 @@
Django-dbfilestorage
--------------------
Custom file storage for Django that stores file data and content type in the database.
Easy to use for testing when you don't care about a filename, and just want to test file data.
Intendted to be used in tests, never in production.
TODO
====
More Tests
Different django and different python versions.
CHANGELOG
=========
2016-12-07 [Tyrel Souza] Initial commits and basic project setup

View File

4
dbfilestorage/admin.py Normal file
View File

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import DBFile
admin.site.register(DBFile)

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-07 21:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='DBFile',
fields=[
('name', models.CharField(max_length=100,
primary_key=True,
serialize=False)),
('content_type', models.CharField(max_length=100)),
('b64', models.TextField()),
],
),
]

View File

17
dbfilestorage/models.py Normal file
View File

@ -0,0 +1,17 @@
from django.db import models
class DBFile(models.Model):
""" Model to store and access uploaded files """
# This is kept as `name` and not something like `md5` because the file
# operations pass around `name` as the identifier, so it's kept the same
# to make sense.
name = models.CharField(max_length=100, primary_key=True)
# file data
content_type = models.CharField(max_length=100)
b64 = models.TextField()
def __unicode__(self):
return unicode(self.name)

91
dbfilestorage/storage.py Normal file
View File

@ -0,0 +1,91 @@
import mimetypes
import logging
import hashlib
from django.db.transaction import atomic
from django.core.files.base import ContentFile
from django.core.files.storage import Storage
from django.core.urlresolvers import reverse
from .models import DBFile
L = logging.getLogger(__name__)
class DBStorage(Storage):
"""
This is the Test Database file upload storage backend.
This is used so that in our test database we always have uploaded
files.
To read more about how to set it up and configure it:
https://docs.djangoproject.com/en/1.8/howto/custom-file-storage
"""
def _open(self, name, mode='rb'):
the_file = DBFile.objects.get(pk=name)
return ContentFile(the_file.b64.decode('base64'))
@atomic
def _save(self, name, content, max_length=None):
"""
The save method does most of the 'magic'.
It stores the contents of the file as a base64 string.
It then takes the filename, and tries to get the mimetype from that (for rendering)
Then it takes the md5 of the read file and uses that as the "unique" key to access the file.
Then it checks if the file exists and if it doesn't, it will create the entry in the database.
:return str: the name(md5) to look up the file by.
"""
ct = None
if hasattr(content.file, "read"):
read_data = content.file.read()
else:
read_data = content.file.encode('utf8')
b64 = read_data.encode('base64')
# Try to get the real content_type if applicable.
try:
if hasattr(content, 'content_type'):
ct = content.content_type
elif hasattr(content.file, 'content_type'):
ct = content.file.content_type
except AttributeError:
pass
# USE mimetypes.guess_type as a fallback.
if ct is None:
# https://docs.python.org/2/library/mimetypes.html
ct = mimetypes.guess_type(name)[0]
# After we get the mimetype by name potentially, mangle it.
name = hashlib.md5(read_data).hexdigest()
# create the file, or just return name if the exact file already exists
if not DBFile.objects.filter(pk=name).exists():
the_file = DBFile(
name=name,
content_type=ct,
b64=b64)
the_file.save()
return name
def get_available_name(self, name, max_length=None):
return name
def path(self, name):
return name
def delete(self, name):
assert name, "The name argument is not allowed to be empty."
DBFile.objects.get(pk=name).delete()
def exists(self, name):
return DBFile.objects.filter(pk=name).exists()
def size(self, name):
dbf = DBFile.objects.get(pk=name)
return len(dbf.b64)
def url(self, name):
return reverse('dbstorage_file', args=(name,))

7
dbfilestorage/urls.py Normal file
View File

@ -0,0 +1,7 @@
from django.conf.urls import patterns, url
import views
urlpatterns = patterns(
'',
url(r'^(?P<name>.*)$', views.show_file, name="dbstorage_file"),
)

18
dbfilestorage/views.py Normal file
View File

@ -0,0 +1,18 @@
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from .models import DBFile
def show_file(request, name):
"""
Get the file object referenced by :name:
Render the decoded base64 representation of the file,
applying the content_type (or closest representation)
:return HttpResponse: Rendered file
"""
dbf = get_object_or_404(DBFile, pk=name)
return HttpResponse(
dbf.b64.decode('base64'),
content_type=dbf.content_type)

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = DjangoDBStorage
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

156
docs/conf.py Normal file
View File

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
#
# Django DBStorage documentation build configuration file, created by
# sphinx-quickstart on Wed Dec 7 14:50:49 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Django DBStorage'
copyright = u'2016, Tyrel Souza'
author = u'Tyrel Souza'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'0.0.1'
# The full version, including alpha/beta/rc tags.
release = u'0.0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'DjangoDBStoragedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'DjangoDBStorage.tex', u'Django DBStorage Documentation',
u'Tyrel Souza', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'djangodbstorage', u'Django DBStorage Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'DjangoDBStorage', u'Django DBStorage Documentation',
author, 'DjangoDBStorage', 'One line description of project.',
'Miscellaneous'),
]

20
docs/index.rst Normal file
View File

@ -0,0 +1,20 @@
.. Django DB File Storage documentation master file, created by
sphinx-quickstart on Wed Dec 7 14:50:49 2016.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Django DB File Storage's documentation!
==================================================
.. toctree::
:maxdepth: 2
:caption: Contents:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

36
docs/make.bat Normal file
View File

@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=DjangoDBStorage
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Django==1.10.4
Sphinx==1.5

45
setup.py Normal file
View File

@ -0,0 +1,45 @@
import re
import os
try:
from setuptools import setup
except ImportError:
from ez_setup import use_setuptools
use_setuptools()
from setuptools import setup
setup(
name="django-dbfilestorage",
version="0.0.1",
description="Database backed file storage for testing.",
long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.",
author="Tyrel Souza",
author_email="john.doe@example.com",
url="https://gitlab.com/tyrelsouza/django-dbfilestorage",
download_url="https://gitlab.com/tyrelsouza/django-dbfilestorage.git",
license="MIT License",
packages=[
"dbfilestorage",
],
include_package_data=True,
install_requires=[
"Django>=1.8.0",
],
tests_require=[
"nose",
"coverage",
],
zip_safe=False,
test_suite="tests.runtests.start",
classifiers=[
"Operating System :: OS Independent",
"Development Status :: 3 - Alpha",
"Environment :: Web Environment",
"Framework :: Django",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries :: Python Modules",
]
)

31
tests/__init__.py Normal file
View File

@ -0,0 +1,31 @@
from __future__ import absolute_import, unicode_literals
import os
test_runner = None
old_config = None
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings"
import django
if hasattr(django, "setup"):
django.setup()
def setup():
global test_runner
global old_config
# If you want to support Django 1.5 and older, you need
# this try-except block.
try:
from django.test.runner import DiscoverRunner
test_runner = DiscoverRunner()
except ImportError:
from django.test.simple import DjangoTestSuiteRunner
test_runner = DjangoTestSuiteRunner()
test_runner.setup_test_environment()
old_config = test_runner.setup_databases()
def teardown():
test_runner.teardown_databases(old_config)
test_runner.teardown_test_environment()

22
tests/runtests.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
from __future__ import absolute_import, print_function, unicode_literals
import os
import sys
import nose
def start(argv=None):
sys.exitfunc = lambda: sys.stderr.write("Shutting down...\n")
if argv is None:
argv = [
"nosetests", "--cover-branches", "--with-coverage",
"--cover-erase", "--verbose",
"--cover-package=dbfilestorage",
]
nose.run_exit(argv=argv, defaultTest=os.path.abspath(os.path.dirname(__file__)))
if __name__ == "__main__":
start(sys.argv)

121
tests/settings.py Normal file
View File

@ -0,0 +1,121 @@
"""
Django settings for tests project.
Generated by 'django-admin startproject' using Django 1.10.4.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_95vgv#3blxe^171sc3*3yjc4z%*10id0hb9-^$#p3ekiwu71a'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'dbfilestorage',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'tests.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'tests.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'

BIN
tests/test_files/kris.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

56
tests/tests.py Normal file
View File

@ -0,0 +1,56 @@
import hashlib
import os
from dbfilestorage.models import DBFile
from django.core.files.storage import default_storage
from django.test import TestCase
from django.test.utils import override_settings
PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__))
class DBFileTest(TestCase):
def setUp(self):
self.filename = "kris.jpg"
self.filepath = os.path.join(PROJECT_ROOT, "test_files", self.filename)
self.md5 = hashlib.md5(open(self.filepath, 'rb').read()).hexdigest()
def _upload(self):
with open(self.filepath, 'rb') as f:
return default_storage.save(self.filepath, f)
@override_settings(
DEFAULT_FILE_STORAGE="dbfilestorage.storage.DBStorage")
def test_upload(self):
""" Test that the file storage uploads and puts in DB Properly """
name = self._upload()
self.assertEqual(name, self.md5)
self.assertTrue(DBFile.objects.filter(name=name).exists())
@override_settings(
DEFAULT_FILE_STORAGE="dbfilestorage.storage.DBStorage")
def test_equality(self):
""" Test that the DB entry matches what is expected from the file """
name = self._upload()
with open(self.filepath, 'rb') as f:
dbf = DBFile.objects.get(name=name)
self.assertEqual(dbf.b64.decode("base64"),
f.read())
self.assertEqual(dbf.content_type, 'image/jpeg')
@override_settings(
DEFAULT_FILE_STORAGE="dbfilestorage.storage.DBStorage")
def test_open(self):
""" Test that the storage mechanism can upload """
name = self._upload()
dbf = default_storage.open(name)
with open(self.filepath, 'rb') as f:
self.assertEqual(dbf.read(), f.read())
@override_settings(
DEFAULT_FILE_STORAGE="dbfilestorage.storage.DBStorage")
def test_exists(self):
""" Test that the storage mechanism can check existance """
name = self._upload()
self.assertTrue(default_storage.exists(name))

21
tests/urls.py Normal file
View File

@ -0,0 +1,21 @@
"""tests URL Configuration
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
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
]

16
tests/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
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
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
application = get_wsgi_application()