From: Radek Czajka Date: Wed, 2 Oct 2019 20:52:45 +0000 (+0200) Subject: Tested for Django 1.6-2.2 X-Git-Tag: 2.3 X-Git-Url: https://git.mdrn.pl/django-sponsors.git/commitdiff_plain/refs/heads/master Tested for Django 1.6-2.2 --- diff --git a/.gitignore b/.gitignore index ea2e8e1..9ca1c82 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ nosetests.xml build dist *.egg-info +/.tox +/htmlcov # Mac OS X garbage .DS_Store diff --git a/example_project/core/__init__.py b/example_project/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example_project/core/settings.py b/example_project/core/settings.py new file mode 100644 index 0000000..55b530c --- /dev/null +++ b/example_project/core/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for core project. + +Generated by 'django-admin startproject' using Django 2.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/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/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '!k@z4ry3s_m%jhu(tikkf&07d#yz(v8fc6x5-(xfbpxn+5fci&' + +# 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', + + 'sponsors', +] + +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 = 'core.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 = 'core.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.2/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/2.2/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/2.2/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/2.2/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/example_project/core/urls.py b/example_project/core/urls.py new file mode 100644 index 0000000..c57b9bc --- /dev/null +++ b/example_project/core/urls.py @@ -0,0 +1,21 @@ +"""core URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.conf.urls import url + +urlpatterns = [ + url('^admin/', admin.site.urls), +] diff --git a/example_project/core/wsgi.py b/example_project/core/wsgi.py new file mode 100644 index 0000000..fb78e28 --- /dev/null +++ b/example_project/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for core 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/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_wsgi_application() diff --git a/example_project/manage.py b/example_project/manage.py new file mode 100755 index 0000000..90063ad --- /dev/null +++ b/example_project/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + + from django.core.management import execute_from_command_line + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/example_project/tests.py b/example_project/tests.py new file mode 100644 index 0000000..ac7ed47 --- /dev/null +++ b/example_project/tests.py @@ -0,0 +1,35 @@ +from django.core.files.base import ContentFile +from django.test import TestCase +from sponsors.models import Sponsor, SponsorPage +from PIL import Image + +try: + from io import BytesIO +except ImportError: + # Python 2 + from StringIO import StringIO as BytesIO + + +class SponsorsTest(TestCase): + def test_empty_page(self): + page = SponsorPage(name='test') + page.save() + + def test_simple_sponsor(self): + s = Sponsor(name='Test Sponsor') + im = Image.new('1', (1,1)) + b = BytesIO() + im.save(b, 'PNG') + s.logo.save('test.png', ContentFile(b.getvalue()), save=True) + + page = SponsorPage( + name='Sponsor', + sponsors=[ + {"name": "empty-column", "sponsors": []}, + {"name": "test-column", "sponsors": [s.id]}, + ]) + page.save() + self.assertNotIn('empty-column', page._html) + self.assertIn('test-column', page._html) + self.assertIn('Test Sponsor', page._html) + diff --git a/setup.py b/setup.py index 77e2826..158f92d 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def whole_trees(package_dir, paths): setup( name='django-sponsors', - version='2.2', + version='2.3', author='Marek Stępniowski', author_email='marek@stepniowski.com', maintainer='Radek Czajka', @@ -34,5 +34,8 @@ setup( license='LICENSE', description='Manage your lists of sponsors with Django admin.', long_description=open('README.md').read(), - install_requires=['jsonfield'], + install_requires=[ + 'Pillow', + 'jsonfield', + ], ) diff --git a/sponsors/__init__.py b/sponsors/__init__.py index 2771385..ecb2cab 100644 --- a/sponsors/__init__.py +++ b/sponsors/__init__.py @@ -3,5 +3,5 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # __author__ = 'Marek Stępniowski, ' -__version__ = '2.2' +__version__ = '2.3' __maintainer__ = 'Radek Czajka ' diff --git a/sponsors/models.py b/sponsors/models.py index 9e7df0c..26e2f9b 100644 --- a/sponsors/models.py +++ b/sponsors/models.py @@ -2,16 +2,23 @@ # This file is part of django-sponsors, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # +from __future__ import unicode_literals + import time -from StringIO import StringIO +try: + from io import BytesIO +except ImportError: + # Python 2 + from StringIO import StringIO as BytesIO + from django.conf import settings +from django.core.files.base import ContentFile from django.db import models from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string from PIL import Image - from jsonfield import JSONField -from django.core.files.base import ContentFile + THUMB_WIDTH = getattr(settings, 'SPONSORS_THUMB_WIDTH', 120) THUMB_HEIGHT = getattr(settings, 'SPONSORS_THUMB_HEIGHT', 120) @@ -77,6 +84,10 @@ class SponsorPage(models.Model): w, h = sponsor.size() total_width = max(total_width, w) total_height += h + + if not total_height: + return + sprite = Image.new('RGBA', (total_width, total_height)) offset = 0 for i, sponsor_id in enumerate(sponsor_ids): @@ -93,11 +104,11 @@ class SponsorPage(models.Model): ) simg = simg.resize(size, Image.ANTIALIAS) sprite.paste(simg, ( - (thumb_size[0] - simg.size[0]) / 2, - offset + (thumb_size[1] - simg.size[1]) / 2, + int((thumb_size[0] - simg.size[0]) / 2), + int(offset + (thumb_size[1] - simg.size[1]) / 2), )) offset += thumb_size[1] - imgstr = StringIO() + imgstr = BytesIO() sprite.save(imgstr, 'png') if self.sprite: diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0909a07 --- /dev/null +++ b/tox.ini @@ -0,0 +1,42 @@ +[tox] +envlist = + clean, + d16-py27, + d17-py{27,34}, + d{18,19,110}-py{27,34,35}, + d111-py{27,34,35,36,37}, + d20-py{34,35,36,37}, + d{21,22}-py{35,36,37}, + stats + +[testenv] +deps = + d16: Django>=1.6,<1.7 + d17: Django>=1.7,<1.8 + d18: Django>=1.8,<1.9 + d19: Django>=1.9,<1.10 + d110: Django>=1.10,<1.11 + d111: Django>=1.11,<2.0 + d20: Django>=2.0,<2.1 + d21: Django>=2.1,<2.2 + d22: Django>=2.2,<2.3 + coverage +commands = + coverage run --append example_project/manage.py test example_project +install_command = pip install --extra-index-url https://py.mdrn.pl/simple {packages} + +[testenv:clean] +commands = + coverage erase +deps = coverage + +[testenv:stats] +commands = + coverage report + coverage html +deps = coverage + +[coverage:run] +branch = true +source = sponsors +