From 592373d1d37d90219d00cd77534d3ce3b7f8dd25 Mon Sep 17 00:00:00 2001 From: Tyrel Souza Date: Wed, 7 Dec 2016 22:50:48 -0500 Subject: [PATCH] Initial commit after moving from Gitlab --- .gitignore | 28 ++++ .gitlab-ci.yml | 14 ++ LICENSE.txt | 22 ++++ README.rst | 21 +++ dbfilestorage/__init__.py | 0 dbfilestorage/admin.py | 4 + dbfilestorage/migrations/0001_initial.py | 26 ++++ dbfilestorage/migrations/__init__.py | 0 dbfilestorage/models.py | 17 +++ dbfilestorage/storage.py | 91 +++++++++++++ dbfilestorage/urls.py | 7 + dbfilestorage/views.py | 18 +++ docs/Makefile | 20 +++ docs/conf.py | 156 +++++++++++++++++++++++ docs/index.rst | 20 +++ docs/make.bat | 36 ++++++ manage.py | 22 ++++ requirements.txt | 2 + setup.py | 45 +++++++ tests/__init__.py | 31 +++++ tests/runtests.py | 22 ++++ tests/settings.py | 121 ++++++++++++++++++ tests/test_files/kris.jpg | Bin 0 -> 35784 bytes tests/tests.py | 56 ++++++++ tests/urls.py | 21 +++ tests/wsgi.py | 16 +++ 26 files changed, 816 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 LICENSE.txt create mode 100644 README.rst create mode 100644 dbfilestorage/__init__.py create mode 100644 dbfilestorage/admin.py create mode 100644 dbfilestorage/migrations/0001_initial.py create mode 100644 dbfilestorage/migrations/__init__.py create mode 100644 dbfilestorage/models.py create mode 100644 dbfilestorage/storage.py create mode 100644 dbfilestorage/urls.py create mode 100644 dbfilestorage/views.py create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100755 manage.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/runtests.py create mode 100644 tests/settings.py create mode 100644 tests/test_files/kris.jpg create mode 100644 tests/tests.py create mode 100644 tests/urls.py create mode 100644 tests/wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..186729c --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..dba3967 --- /dev/null +++ b/.gitlab-ci.yml @@ -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 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..a83f19c --- /dev/null +++ b/LICENSE.txt @@ -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. + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8e71fa5 --- /dev/null +++ b/README.rst @@ -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 diff --git a/dbfilestorage/__init__.py b/dbfilestorage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dbfilestorage/admin.py b/dbfilestorage/admin.py new file mode 100644 index 0000000..53bf1f2 --- /dev/null +++ b/dbfilestorage/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import DBFile + +admin.site.register(DBFile) diff --git a/dbfilestorage/migrations/0001_initial.py b/dbfilestorage/migrations/0001_initial.py new file mode 100644 index 0000000..794d98c --- /dev/null +++ b/dbfilestorage/migrations/0001_initial.py @@ -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()), + ], + ), + ] diff --git a/dbfilestorage/migrations/__init__.py b/dbfilestorage/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dbfilestorage/models.py b/dbfilestorage/models.py new file mode 100644 index 0000000..979a395 --- /dev/null +++ b/dbfilestorage/models.py @@ -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) diff --git a/dbfilestorage/storage.py b/dbfilestorage/storage.py new file mode 100644 index 0000000..c716901 --- /dev/null +++ b/dbfilestorage/storage.py @@ -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,)) diff --git a/dbfilestorage/urls.py b/dbfilestorage/urls.py new file mode 100644 index 0000000..1fe30a0 --- /dev/null +++ b/dbfilestorage/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import patterns, url +import views + +urlpatterns = patterns( + '', + url(r'^(?P.*)$', views.show_file, name="dbstorage_file"), +) diff --git a/dbfilestorage/views.py b/dbfilestorage/views.py new file mode 100644 index 0000000..8f04488 --- /dev/null +++ b/dbfilestorage/views.py @@ -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) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..68477f0 --- /dev/null +++ b/docs/Makefile @@ -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) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..19ff111 --- /dev/null +++ b/docs/conf.py @@ -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'), +] + + + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7d99198 --- /dev/null +++ b/docs/index.rst @@ -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` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..3bea410 --- /dev/null +++ b/docs/make.bat @@ -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 diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..38a919f --- /dev/null +++ b/manage.py @@ -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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..edde82d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Django==1.10.4 +Sphinx==1.5 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1d53b47 --- /dev/null +++ b/setup.py @@ -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", + ] +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..45225ce --- /dev/null +++ b/tests/__init__.py @@ -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() diff --git a/tests/runtests.py b/tests/runtests.py new file mode 100644 index 0000000..371dfd1 --- /dev/null +++ b/tests/runtests.py @@ -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) diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..3ead56d --- /dev/null +++ b/tests/settings.py @@ -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/' diff --git a/tests/test_files/kris.jpg b/tests/test_files/kris.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a6831d807c6b8cb3dc46edd50811c6624bbdcc3 GIT binary patch literal 35784 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 Q7}N9B{JNDpd~U!0+3C?@aR2}S literal 0 HcmV?d00001 diff --git a/tests/tests.py b/tests/tests.py new file mode 100644 index 0000000..170b37f --- /dev/null +++ b/tests/tests.py @@ -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)) diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 0000000..87abfd0 --- /dev/null +++ b/tests/urls.py @@ -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), +] diff --git a/tests/wsgi.py b/tests/wsgi.py new file mode 100644 index 0000000..0a2bd3d --- /dev/null +++ b/tests/wsgi.py @@ -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()