├── image_gallery ├── tests │ ├── __init__.py │ ├── utils.py │ ├── urls.py │ ├── settings.py │ ├── views_tests.py │ ├── cmsplugin_tests.py │ ├── tags_tests.py │ ├── models_tests.py │ └── test_settings.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── image_gallery_tags.py ├── __init__.py ├── templates │ ├── standard.html │ ├── image_gallery │ │ ├── gallery_detail.html │ │ ├── pictures.html │ │ ├── partials │ │ │ └── gallery.html │ │ └── gallery_list.html │ └── base.html ├── locale │ └── de │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── cms_app.py ├── urls.py ├── app_settings.py ├── cms_plugins.py ├── admin.py ├── views.py └── models.py ├── requirements.txt ├── DESCRIPTION ├── test_requirements.txt ├── .flake8 ├── .gitignore ├── AUTHORS ├── tox.ini ├── MANIFEST.in ├── manage.py ├── setup.py ├── LICENSE ├── runtests.py ├── CHANGELOG.txt └── README.rst /image_gallery/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image_gallery/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image_gallery/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-cms 3 | django-filer 4 | Pillow 5 | -------------------------------------------------------------------------------- /image_gallery/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.8.0' 3 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | A reusable Django app adding django-filer-based galleries to Django-CMS. 2 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | fabric3 2 | coverage 3 | tox 4 | flake8 5 | django-libs 6 | ipdb 7 | model_bakery 8 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E402, W503, C901 3 | max-line-length = 120 4 | max-complexity = 15 5 | exclude = */migrations/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | .tox 4 | coverage/ 5 | .coverage 6 | db.sqlite 7 | dist/ 8 | static/ 9 | media/ 10 | .idea/ 11 | -------------------------------------------------------------------------------- /image_gallery/templates/standard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load cms_tags %} 3 | {% block main %}{% placeholder content %}{% endblock %} -------------------------------------------------------------------------------- /image_gallery/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/cmsplugin-image-gallery/HEAD/image_gallery/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /image_gallery/tests/utils.py: -------------------------------------------------------------------------------- 1 | from filer.models import Folder 2 | 3 | 4 | def generate_filer_folder(): 5 | folder = Folder() 6 | folder.save() 7 | return folder 8 | -------------------------------------------------------------------------------- /image_gallery/templates/image_gallery/gallery_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block main %} 4 | {% include "image_gallery/partials/gallery.html" %} 5 | {% endblock %} -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current or previous core committers 2 | 3 | Tobias Lorenz 4 | 5 | Contributors (in alphabetical order) 6 | 7 | * Tristan Fischer (dersphere) 8 | * Your name could stand here :) 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37-django2 3 | 4 | [testenv] 5 | usedevelop = True 6 | deps = 7 | django2: Django>=2.0,<3 8 | -rtest_requirements.txt 9 | commands = python runtests.py 10 | -------------------------------------------------------------------------------- /image_gallery/templates/image_gallery/pictures.html: -------------------------------------------------------------------------------- 1 | {% load thumbnail %} 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include DESCRIPTION 4 | include CHANGELOG.txt 5 | include README.md 6 | graft image_gallery 7 | global-exclude *.orig *.pyc *.log *.swp 8 | prune image_gallery/tests/coverage 9 | prune image_gallery/.ropeproject 10 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", 8 | "image_gallery.tests.settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /image_gallery/cms_app.py: -------------------------------------------------------------------------------- 1 | """CMS apphook for the ``image_gallery`` app.""" 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from cms.app_base import CMSApp 5 | from cms.apphook_pool import apphook_pool 6 | 7 | 8 | class ImageGalleryApphook(CMSApp): 9 | name = _("Image Gallery Apphook") 10 | urls = ["image_gallery.urls"] 11 | 12 | 13 | apphook_pool.register(ImageGalleryApphook) 14 | -------------------------------------------------------------------------------- /image_gallery/urls.py: -------------------------------------------------------------------------------- 1 | """URLs for the ``image_gallery`` app.""" 2 | from django.conf.urls import url 3 | from django.views.generic import DetailView 4 | 5 | from .models import Gallery 6 | from .views import GalleryListView 7 | 8 | 9 | urlpatterns = [ 10 | url(r'^(?P\d+)/$', 11 | DetailView.as_view(model=Gallery), 12 | name='image_gallery_detail'), 13 | url(r'^$', GalleryListView.as_view(), name='image_gallery_list'), 14 | ] 15 | -------------------------------------------------------------------------------- /image_gallery/templates/image_gallery/partials/gallery.html: -------------------------------------------------------------------------------- 1 | {% load i18n thumbnail cms_tags %} 2 |

{{ gallery.title }}

3 |

{% if gallery.date %}{{ gallery.date|date }}, {% endif %}{{ gallery.location }}

4 |

{% render_placeholder gallery.description "description" %}

5 | 10 | -------------------------------------------------------------------------------- /image_gallery/app_settings.py: -------------------------------------------------------------------------------- 1 | """Settings of the ``image_gallery``` application.""" 2 | from django.conf import settings 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | GALLERY_DISPLAY_TYPE_CHOICES_DEFAULT = ( 6 | ('default', _('Default')), 7 | ('teaser', _('Teaser')), 8 | ) 9 | 10 | PAGINATION_AMOUNT = getattr(settings, 'GALLERY_PAGINATION_AMOUNT', 10) 11 | DISPLAY_TYPE_CHOICES = getattr( 12 | settings, 13 | 'GALLERY_DISPLAY_TYPE_CHOICES', 14 | GALLERY_DISPLAY_TYPE_CHOICES_DEFAULT 15 | ) 16 | -------------------------------------------------------------------------------- /image_gallery/templates/image_gallery/gallery_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n thumbnail %} 3 | 4 | {% block main %} 5 | {% for gallery in object_list %} 6 |
7 | {% if gallery.get_folder_images.all %} 8 | 9 | {% endif %} 10 |

{{ gallery.title }}

11 | {% if gallery.date %}{{ gallery.date|date }}, {% endif %}{{ gallery.location }} 12 |
13 | {% endfor %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /image_gallery/tests/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | This ``urls.py`` is only used when running the tests via ``runtests.py``. 3 | As you know, every app must be hooked into yout main ``urls.py`` so that 4 | you can actually reach the app's views (provided it has any views, of course). 5 | 6 | """ 7 | from django.conf import settings 8 | from django.conf.urls import include, url 9 | from django.conf.urls.static import static 10 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 11 | 12 | 13 | urlpatterns = [ 14 | url(r'^app/', include('image_gallery.urls')), 15 | url(r'^', include('cms.urls')), 16 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 17 | 18 | urlpatterns += staticfiles_urlpatterns() 19 | -------------------------------------------------------------------------------- /image_gallery/cms_plugins.py: -------------------------------------------------------------------------------- 1 | """CMS Plugins for the ``image_gallery`` app.""" 2 | from django.utils.translation import ugettext as _ 3 | 4 | from cms.plugin_base import CMSPluginBase 5 | from cms.plugin_pool import plugin_pool 6 | 7 | from .models import GalleryPlugin 8 | 9 | 10 | class CMSGalleryPlugin(CMSPluginBase): 11 | model = GalleryPlugin 12 | name = _('Filer Gallery') 13 | render_template = 'image_gallery/partials/gallery.html' 14 | 15 | def render(self, context, instance, placeholder): 16 | context.update({ 17 | 'gallery': instance.gallery, 18 | 'images': instance.gallery.get_folder_images(), 19 | 'placeholder': placeholder, 20 | 'display_type': instance.display_type, 21 | }) 22 | return context 23 | 24 | 25 | plugin_pool.register_plugin(CMSGalleryPlugin) 26 | -------------------------------------------------------------------------------- /image_gallery/tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | These settings are used by the ``manage.py`` command. 3 | With normal tests we want to use the fastest possible way which is an 4 | in-memory sqlite database but if you want to create South south_migrations you 5 | need a persistant database. 6 | Unfortunately there seems to be an issue with either South or syncdb so that 7 | defining two routers ("default" and "south") does not work. 8 | """ 9 | from distutils.version import StrictVersion 10 | 11 | import django 12 | 13 | from .test_settings import * # NOQA 14 | 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': 'db.sqlite', 20 | } 21 | } 22 | 23 | django_version = django.get_version() 24 | if StrictVersion(django_version) < StrictVersion('1.7'): 25 | INSTALLED_APPS.append('south', ) 26 | -------------------------------------------------------------------------------- /image_gallery/tests/views_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the views of the ``image_gallery`` app.""" 2 | from django.test import TestCase 3 | 4 | from django_libs.tests.mixins import ViewRequestFactoryTestMixin 5 | from model_bakery import baker 6 | 7 | from .utils import generate_filer_folder 8 | from .. import views 9 | 10 | baker.generators.add('filer.fields.folder.FilerFolderField', 11 | generate_filer_folder) 12 | 13 | 14 | class GalleryListViewTestCase(ViewRequestFactoryTestMixin, TestCase): 15 | """Tests for the ``GalleryListView`` view class.""" 16 | view_class = views.GalleryListView 17 | 18 | def setUp(self): 19 | self.gallery = baker.make( 20 | 'image_gallery.Gallery', 21 | category=baker.make('image_gallery.GalleryCategory')) 22 | 23 | def test_view(self): 24 | self.is_callable() 25 | self.is_callable(data={'category': self.gallery.category.slug}) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | import image_gallery 4 | 5 | 6 | def read(fname): 7 | try: 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | except IOError: 10 | return '' 11 | 12 | 13 | setup( 14 | name="cmsplugin-image-gallery", 15 | version=image_gallery.__version__, 16 | description=read('DESCRIPTION'), 17 | long_description=read('README.rst'), 18 | license='The MIT License', 19 | platforms=['OS Independent'], 20 | keywords='django, filer, gallery, django-filer, image', 21 | author='Tobias Lorenz', 22 | author_email='tobias.lorenz@bitmazk.com', 23 | url="https://github.com/bitmazk/cmsplugin-image-gallery", 24 | packages=find_packages(), 25 | include_package_data=True, 26 | install_requires=[ 27 | 'Django', 28 | 'django-cms', 29 | 'django-filer', 30 | 'Pillow', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /image_gallery/admin.py: -------------------------------------------------------------------------------- 1 | """Simple admin registration for ``image_gallery`` models.""" 2 | from django.contrib import admin 3 | 4 | from cms.admin.placeholderadmin import PlaceholderAdminMixin 5 | from filer.admin.imageadmin import ImageAdmin 6 | 7 | from . import models 8 | 9 | 10 | class GalleryAdmin(PlaceholderAdminMixin, admin.ModelAdmin): 11 | """Custom admin for the ``Gallery`` model.""" 12 | list_display = ('title', 'date', 'location', 'folder', 'category') 13 | list_filter = ['category', ] 14 | 15 | 16 | class GalleryCategoryAdmin(admin.ModelAdmin): 17 | """Custom admin for the ``GalleryCategory`` model.""" 18 | list_display = ('name', 'slug') 19 | 20 | 21 | class GalleryImageInline(admin.TabularInline): 22 | model = models.GalleryImageExtension 23 | 24 | 25 | admin.site.register(models.GalleryCategory, GalleryCategoryAdmin) 26 | admin.site.register(models.Gallery, GalleryAdmin) 27 | ImageAdmin.inlines = ImageAdmin.inlines[:] + [GalleryImageInline] 28 | -------------------------------------------------------------------------------- /image_gallery/templatetags/image_gallery_tags.py: -------------------------------------------------------------------------------- 1 | """Template tags for the ``image_gallery`` app.""" 2 | from django import template 3 | 4 | from filer.models import Image 5 | 6 | from image_gallery.models import Gallery 7 | 8 | register = template.Library() 9 | 10 | 11 | @register.inclusion_tag('image_gallery/pictures.html', takes_context=True) 12 | def render_pictures(context, selection='recent', amount=3): 13 | """Template tag to render a list of pictures.""" 14 | pictures = Image.objects.filter( 15 | folder__id__in=Gallery.objects.filter(is_published=True).values_list( 16 | 'folder__pk', flat=True)) 17 | if selection == 'recent': 18 | context.update({ 19 | 'pictures': pictures.order_by('-uploaded_at')[:amount] 20 | }) 21 | elif selection == 'random': 22 | context.update({ 23 | 'pictures': pictures.order_by('?')[:amount] 24 | }) 25 | else: 26 | return None 27 | return context 28 | -------------------------------------------------------------------------------- /image_gallery/views.py: -------------------------------------------------------------------------------- 1 | """Views for the ``image_gallery`` app.""" 2 | from django.views.generic import ListView 3 | 4 | from .app_settings import PAGINATION_AMOUNT 5 | from .models import Gallery, GalleryCategory 6 | 7 | 8 | class GalleryListView(ListView): 9 | """View to display a list of ``Gallery`` instances.""" 10 | paginate_by = PAGINATION_AMOUNT 11 | 12 | def get_queryset(self): 13 | self.category = self.request.GET.get('category') 14 | if self.category: 15 | return Gallery.objects.filter( 16 | is_published=True, category__slug=self.category).order_by( 17 | '-date') 18 | return Gallery.objects.filter(is_published=True).order_by('-date') 19 | 20 | def get_context_data(self, **kwargs): 21 | ctx = super(GalleryListView, self).get_context_data(**kwargs) 22 | ctx.update({'categories': GalleryCategory.objects.all()}) 23 | if self.category: 24 | ctx.update({'active_category': self.category}) 25 | return ctx 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012 Martin Brochhaus 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This script is used to run tests, create a coverage report and output the 4 | statistics at the end of the tox run. 5 | To run this script just execute ``tox`` 6 | """ 7 | import re 8 | 9 | from fabric.api import local, warn 10 | from fabric.colors import green, red 11 | 12 | if __name__ == '__main__': 13 | local('flake8 --ignore=E126 --ignore=W391 --ignore=F405 --statistics' 14 | ' --exclude=submodules,south_migrations,migrations,build' 15 | ',dist,site-packages,.tox .') 16 | local('coverage run --source="image_gallery" manage.py test -v 2' 17 | ' --traceback --failfast' 18 | ' --settings=image_gallery.tests.settings' 19 | ' --pattern="*_tests.py"') 20 | local('coverage html -d coverage --omit="*__init__*,*/settings/*,' 21 | '*/south_migrations/*,*/migrations/*,*/tests/*,*admin*"') 22 | total_line = local('grep -n pc_cov coverage/index.html', capture=True) 23 | percentage = float(re.findall(r'(\d+)%', total_line)[-1]) 24 | if percentage < 100: 25 | warn(red('Coverage is {0}%'.format(percentage))) 26 | print(green('Coverage is {0}%'.format(percentage))) 27 | -------------------------------------------------------------------------------- /image_gallery/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}{% endblock %} | django-frequently 11 | 12 | 13 | 14 | 15 | {% render_block "css" %} 16 | 17 | 18 | 19 | 20 | 21 | {% block main %}{% endblock %} 22 | 23 | 24 | {% block extrajs %}{% endblock %} 25 | {% render_block "js" %} 26 | 30 | 31 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | === ongoing === 2 | 3 | === 0.7.2 === 4 | 5 | - Prepared app for Django 1.9 and Python 3.5 6 | 7 | === 0.7.1 === 8 | 9 | - Prepared app for Django>=1.7 10 | - Prepared app for django-cms>=3.1.2 11 | 12 | === 0.6.X === 13 | 14 | - Fixed unicode function 15 | 16 | === 0.6.2 === 17 | 18 | - Fixed category ordering 19 | 20 | === 0.6.1 === 21 | 22 | - Added german translation 23 | 24 | === 0.6 === 25 | 26 | - Added display_type to GalleryPlugin model 27 | - Added new setting GALLERY_DISPLAY_TYPE_CHOICES 28 | - Added GalleryImageExtension model 29 | - Added inline for image extensions to Filer's Image admin 30 | 31 | === 0.5 === 32 | 33 | - Updated to last factory_boy 34 | 35 | === 0.4 === 36 | 37 | - Added is_published field to Gallery model 38 | - Changed default sorting of gallery list to descending 39 | 40 | === 0.3.2 === 41 | 42 | - Added get_absolute_url method for Gallery model 43 | 44 | === 0.3.1 === 45 | 46 | - Added categories to the context of the GalleryListView 47 | 48 | === 0.3 === 49 | 50 | - Sorting ListView items via date 51 | - ListView can sort via category when receiving a "category" get parameter 52 | - Added GalleryCategory model 53 | 54 | === 0.2 === 55 | 56 | - Added apphook plus DetailView and ListView 57 | - Added pagination setting 58 | - Removed option to show private images 59 | 60 | === 0.1 === 61 | 62 | - Added a template tag to display picture lists 63 | - Initial commit 64 | -------------------------------------------------------------------------------- /image_gallery/tests/cmsplugin_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for models of the ``image_gallery``` application.""" 2 | from django.contrib.auth.models import AnonymousUser 3 | from django.contrib.sessions.middleware import SessionMiddleware 4 | from django.template.context import RequestContext 5 | from django.test import TestCase 6 | from django.test.client import RequestFactory 7 | 8 | # from filer.models import Folder 9 | # from model_bakery import baker 10 | 11 | # from ..cms_plugins import CMSGalleryPlugin 12 | 13 | 14 | class CMSGalleryPluginTestCase(TestCase): 15 | """Tests for the ``CMSGalleryPlugin`` cmsplugin.""" 16 | longMessage = True 17 | 18 | def setUp(self): 19 | # create context mock 20 | request = RequestFactory().get('/') 21 | request.user = AnonymousUser() 22 | SessionMiddleware().process_request(request) 23 | request.session.save() 24 | self.context = RequestContext(request) 25 | # folder = Folder() 26 | # folder.save() 27 | # gallery = baker.make('image_gallery.Gallery', folder=folder) 28 | # self.plugin = baker.make('image_gallery.GalleryPlugin', 29 | # gallery=gallery) 30 | # self.cmsplugin = CMSGalleryPlugin() 31 | 32 | def _test_render(self): 33 | pass 34 | # self.assertEqual( 35 | # self.cmsplugin.render(context=self.context, instance=self.plugin, 36 | # placeholder=None).get('gallery'), 37 | # self.plugin.gallery) 38 | -------------------------------------------------------------------------------- /image_gallery/tests/tags_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the tags of the ``image_gallery`` app.""" 2 | from django.contrib.auth.models import AnonymousUser 3 | from django.contrib.sessions.middleware import SessionMiddleware 4 | from django.template.context import RequestContext 5 | from django.test import TestCase 6 | from django.test.client import RequestFactory 7 | 8 | from model_bakery import baker 9 | 10 | from ..templatetags.image_gallery_tags import render_pictures 11 | 12 | 13 | class RenderPicturesTestCase(TestCase): 14 | """Tests for the ``render_pictures`` tag.""" 15 | longMessage = True 16 | 17 | def setUp(self): 18 | # create context mock 19 | request = RequestFactory().get('/') 20 | request.user = AnonymousUser() 21 | SessionMiddleware().process_request(request) 22 | request.session.save() 23 | self.context = RequestContext(request) 24 | self.gallery = baker.make('image_gallery.Gallery', is_published=True, 25 | folder=baker.make('filer.Folder')) 26 | 27 | def test_tag(self): 28 | # Returns None, because of an invalid selection name 29 | self.assertFalse(render_pictures(self.context, selection='fail')) 30 | 31 | # Returns empty queryset 32 | self.assertFalse(render_pictures(self.context).get('pictures')) 33 | 34 | # Returns two pictures 35 | baker.make('filer.Image', folder=self.gallery.folder) 36 | baker.make('filer.Image', folder=self.gallery.folder) 37 | self.assertEqual( 38 | render_pictures(self.context).get('pictures').count(), 2) 39 | 40 | # Returns one picture, because amount was set to `1` 41 | baker.make('filer.Image', folder=self.gallery.folder) 42 | baker.make('filer.Image', folder=self.gallery.folder) 43 | self.assertEqual(render_pictures(self.context, 'recent', 1).get( 44 | 'pictures').count(), 1) 45 | 46 | # Returns three random pictures 47 | self.assertEqual( 48 | render_pictures(self.context, 'random').get('pictures').count(), 3) 49 | -------------------------------------------------------------------------------- /image_gallery/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the models of the ``image_gallery`` app.""" 2 | from django.test import TestCase 3 | from model_bakery import baker 4 | 5 | from .utils import generate_filer_folder 6 | 7 | baker.generators.add('filer.fields.folder.FilerFolderField', 8 | generate_filer_folder) 9 | 10 | 11 | class GalleryTestCase(TestCase): 12 | """Tests for the ``Gallery`` model class.""" 13 | longMessage = True 14 | 15 | def test_instantiation(self): 16 | """Test if the ``Gallery`` model instantiates.""" 17 | gallery = baker.make('image_gallery.Gallery') 18 | self.assertTrue(str(gallery), msg='Should be correctly instantiated.') 19 | 20 | def test_get_folder_images(self): 21 | """Tests for the model's ``get_folder_images`` function.""" 22 | gallery = baker.make('image_gallery.Gallery') 23 | self.assertEqual(gallery.get_folder_images().count(), 0, msg=( 24 | 'Should return an empty image list.')) 25 | 26 | 27 | class GalleryImageExtensionTestCase(TestCase): 28 | """Tests for the ``GalleryImageExtension`` model class.""" 29 | longMessage = True 30 | 31 | def test_instantiation(self): 32 | obj = baker.make('image_gallery.GalleryImageExtension') 33 | self.assertTrue(str(obj), msg=( 34 | 'Should be able to instantiate and save the object.')) 35 | 36 | 37 | class GalleryCategoryTestCase(TestCase): 38 | """Tests for the ``GalleryCategory`` model class.""" 39 | longMessage = True 40 | 41 | def test_instantiation(self): 42 | """Test instantiation of the ``GalleryCategory`` model.""" 43 | gallerycategory = baker.make('image_gallery.GalleryCategory') 44 | self.assertTrue(str(gallerycategory)) 45 | 46 | 47 | class GalleryPluginTestCase(TestCase): 48 | """Tests for the ``GalleryPlugin`` model class.""" 49 | longMessage = True 50 | 51 | def test_instantiation(self): 52 | """Test instantiation of the ``GalleryPlugin`` model.""" 53 | galleryplugin = baker.make('image_gallery.GalleryPlugin') 54 | self.assertTrue(str(galleryplugin)) 55 | -------------------------------------------------------------------------------- /image_gallery/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-08-15 09:48-0500\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 19 | 20 | #: image_gallery/app_settings.py:6 21 | msgid "Default" 22 | msgstr "Standard" 23 | 24 | #: image_gallery/app_settings.py:7 25 | msgid "Teaser" 26 | msgstr "Teaser" 27 | 28 | #: image_gallery/cms_app.py:9 29 | msgid "Image Gallery Apphook" 30 | msgstr "Bildergalerie Apphook" 31 | 32 | #: image_gallery/cms_plugins.py:12 33 | msgid "Filer Gallery" 34 | msgstr "Bildergalerie" 35 | 36 | #: image_gallery/models.py:30 37 | msgid "Category" 38 | msgstr "Kategorie" 39 | 40 | #: image_gallery/models.py:36 41 | msgid "Title" 42 | msgstr "Titel" 43 | 44 | #: image_gallery/models.py:40 45 | msgid "Date" 46 | msgstr "Datum" 47 | 48 | #: image_gallery/models.py:46 49 | msgid "Location" 50 | msgstr "Ort" 51 | 52 | #: image_gallery/models.py:52 53 | msgid "Description" 54 | msgstr "Beschreibung" 55 | 56 | #: image_gallery/models.py:56 57 | msgid "Folder" 58 | msgstr "Ordner" 59 | 60 | #: image_gallery/models.py:60 61 | msgid "Is published" 62 | msgstr "Ist veröffentlicht" 63 | 64 | #: image_gallery/models.py:66 image_gallery/models.py:178 65 | msgid "Gallery" 66 | msgstr "Galerie" 67 | 68 | #: image_gallery/models.py:67 69 | msgid "Galleries" 70 | msgstr "Galerie" 71 | 72 | #: image_gallery/models.py:126 73 | msgid "Name" 74 | msgstr "Name" 75 | 76 | #: image_gallery/models.py:131 77 | msgid "Slug" 78 | msgstr "Slug" 79 | 80 | #: image_gallery/models.py:139 81 | msgid "Gallery Category" 82 | msgstr "Galerie-Kategorie" 83 | 84 | #: image_gallery/models.py:140 85 | msgid "Gallery Categories" 86 | msgstr "Galerie-Kategorien" 87 | 88 | #: image_gallery/models.py:153 89 | msgid "Image" 90 | msgstr "Bild" 91 | 92 | #: image_gallery/models.py:158 93 | msgid "Is featured image" 94 | msgstr "Ist ein hervorgehobenes Bild" 95 | 96 | #: image_gallery/models.py:162 97 | msgid "Gallery Image Extension" 98 | msgstr "Galeriebild-Erweiterung" 99 | 100 | #: image_gallery/models.py:163 101 | msgid "Gallery Image Extensions" 102 | msgstr "Galeriebild-Erweiterungen" 103 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | CMSplugin Image Gallery 2 | ==================== 3 | 4 | A Django application adding django-filer-based galleries to Django-CMS. 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | You need to install the following prerequisites in order to use this app:: 11 | 12 | pip install Django 13 | pip install django-cms 14 | pip install django-filer 15 | pip install Pillow 16 | 17 | If you want to install the latest stable release from PyPi:: 18 | 19 | $ pip install cmsplugin-image-gallery 20 | 21 | If you feel adventurous and want to install the latest commit from GitHub:: 22 | 23 | $ pip install -e git://github.com/bitmazk/cmsplugin-image-gallery.git#egg=image_gallery 24 | 25 | Add ``image_gallery`` to your ``INSTALLED_APPS``:: 26 | 27 | INSTALLED_APPS = ( 28 | ..., 29 | 'easy_thumbnails', 30 | 'filer', 31 | 'image_gallery', 32 | ) 33 | 34 | 35 | Usage 36 | ----- 37 | 38 | First create a gallery object with a filer folder and set ``is_published`` to 39 | ``True`` once you want to publish the gallery. 40 | 41 | Using the apphook 42 | +++++++++++++++++ 43 | 44 | Simply create a django-cms page and select it in the ``Application`` field of 45 | the ``Advanced Settings``. 46 | 47 | Using the cmsplugin 48 | +++++++++++++++++++ 49 | 50 | Create a CMS page with a placeholder and simply insert the plugin 51 | ``Filer Gallery``. 52 | 53 | Using the template tags 54 | +++++++++++++++++++++++ 55 | 56 | You can also use our template tag to display a list of pictures:: 57 | 58 | {% render_pictures %} 59 | 60 | ...for the last 3 uploaded pictures. You can use the selection parameters 61 | ``recent`` (default) and ``random`` and set an amount of pictures to display:: 62 | 63 | {% render_pictures 'random' 10 %} 64 | 65 | 66 | Settings 67 | -------- 68 | 69 | GALLERY_PAGINATION_AMOUNT 70 | +++++++++++++++++++++++++ 71 | 72 | Default: 10 73 | 74 | Amount of galleries to display in the list view. 75 | 76 | 77 | GALLERY_DISPLAY_TYPE_CHOICES 78 | ++++++++++++++++++++++++++++ 79 | 80 | Default:: 81 | 82 | ( 83 | ('default', _('Default')), 84 | ('teaser', _('Teaser')), 85 | ) 86 | 87 | When you use the ``Filer Gallery`` plugin, you can select the gallery that 88 | should be rendered and a display type. This is useful if one and the same 89 | gallery should be rendrerd in different ways at different places on your 90 | site. The selected value will be added to the plugin template's context with 91 | the variable name ``{{ display_type }}``. 92 | 93 | 94 | Contribute 95 | ---------- 96 | 97 | If you want to contribute to this project, please perform the following steps:: 98 | 99 | # Fork this repository 100 | # Clone your fork 101 | $ mkvirtualenv -p python2.7 cmsplugin-image-gallery 102 | $ pip install -r requirements.txt 103 | $ ./logger/tests/runtests.sh 104 | # You should get no failing tests 105 | 106 | $ git co -b feature_branch master 107 | # Implement your feature and tests 108 | # Describe your change in the CHANGELOG.txt 109 | $ git add . && git commit 110 | $ git push origin feature_branch 111 | # Send us a pull request for your feature branch 112 | 113 | Whenever you run the tests a coverage output will be generated in 114 | ``tests/coverage/index.html``. When adding new features, please make sure that 115 | you keep the coverage at 100%. 116 | 117 | 118 | Roadmap 119 | ------- 120 | 121 | Check the issue tracker on github for milestones and features to come. 122 | -------------------------------------------------------------------------------- /image_gallery/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | """Settings that need to be set in order to run the tests.""" 2 | import os 3 | 4 | 5 | DEBUG = True 6 | SITE_ID = 1 7 | 8 | DATABASES = { 9 | "default": { 10 | "ENGINE": "django.db.backends.sqlite3", 11 | "NAME": ":memory:", 12 | } 13 | } 14 | 15 | ROOT_URLCONF = 'image_gallery.tests.urls' 16 | 17 | PROJECT_ROOT = os.path.realpath( 18 | os.path.join(os.path.dirname(__file__), "../")) 19 | 20 | MEDIA_URL = '/media/' 21 | STATIC_URL = '/static/' 22 | 23 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, '../media/') 24 | STATIC_ROOT = os.path.join(PROJECT_ROOT, '../static/') 25 | 26 | STATICFILES_DIRS = ( 27 | os.path.join(PROJECT_ROOT, 'tests/test_static/'), 28 | ) 29 | 30 | STATICFILES_FINDERS = ( 31 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 32 | 'django.contrib.staticfiles.finders.FileSystemFinder', 33 | 'django.contrib.staticfiles.finders.DefaultStorageFinder', 34 | ) 35 | 36 | TEMPLATES = [{ 37 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 38 | 'APP_DIRS': True, 39 | 'DIRS': [os.path.join(os.path.dirname(__file__), '../templates')], 40 | 'OPTIONS': { 41 | 'context_processors': ( 42 | 'django.template.context_processors.request', 43 | 'django.contrib.auth.context_processors.auth', 44 | 'django.template.context_processors.debug', 45 | 'django.template.context_processors.i18n', 46 | 'django.template.context_processors.media', 47 | 'django.template.context_processors.static', 48 | 'django.template.context_processors.tz', 49 | 'django.contrib.messages.context_processors.messages', 50 | 'sekizai.context_processors.sekizai', 51 | 'cms.context_processors.cms_settings', 52 | ) 53 | } 54 | }] 55 | 56 | EXTERNAL_APPS = [ 57 | 'djangocms_admin_style', 58 | 'django.contrib.admin', 59 | 'django.contrib.admindocs', 60 | 'django.contrib.auth', 61 | 'django.contrib.contenttypes', 62 | 'django.contrib.humanize', 63 | 'django.contrib.messages', 64 | 'django.contrib.sessions', 65 | 'django.contrib.staticfiles', 66 | 'django.contrib.sitemaps', 67 | 'django.contrib.sites', 68 | 'cms', 69 | 'menus', 70 | 'treebeard', 71 | 'sekizai', 72 | 'filer', 73 | 'easy_thumbnails', 74 | ] 75 | 76 | INTERNAL_APPS = [ 77 | 'image_gallery', 78 | ] 79 | 80 | INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS 81 | 82 | LANGUAGE_CODE = 'en' 83 | LANGUAGES = [ 84 | ('en', 'English'), 85 | ] 86 | 87 | MIDDLEWARE = [ 88 | 'django.middleware.common.CommonMiddleware', 89 | 'django.contrib.sessions.middleware.SessionMiddleware', 90 | 'django.middleware.csrf.CsrfViewMiddleware', 91 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 92 | 'django.contrib.messages.middleware.MessageMiddleware', 93 | # 'django.middleware.locale.LocaleMiddleware', 94 | 'cms.middleware.user.CurrentUserMiddleware', 95 | 'cms.middleware.page.CurrentPageMiddleware', 96 | 'cms.middleware.toolbar.ToolbarMiddleware', 97 | 'cms.middleware.language.LanguageCookieMiddleware', 98 | ] 99 | 100 | # django-cms settings 101 | CMS_TEMPLATES = ( 102 | ('standard.html', 'Standard'), 103 | ) 104 | 105 | # easy_thumbnails settings 106 | THUMBNAIL_PROCESSORS = ( 107 | 'easy_thumbnails.processors.colorspace', 108 | 'easy_thumbnails.processors.autocrop', 109 | 'filer.thumbnail_processors.scale_and_crop_with_subject_location', 110 | 'easy_thumbnails.processors.filters', 111 | ) 112 | 113 | SECRET_KEY = 'foo' 114 | -------------------------------------------------------------------------------- /image_gallery/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-05 06:21 2 | 3 | import cms.models.fields 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import filer.fields.folder 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), 16 | ('filer', '0011_auto_20190418_0137'), 17 | # ('cms', '0022_auto_20180620_1551'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='Gallery', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('title', models.CharField(max_length=100, verbose_name='Title')), 26 | ('date', models.DateTimeField(blank=True, null=True, verbose_name='Date')), 27 | ('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='Location')), 28 | ('is_published', models.BooleanField(default=False, verbose_name='Is published')), 29 | ], 30 | options={ 31 | 'verbose_name': 'Gallery', 32 | 'verbose_name_plural': 'Galleries', 33 | 'ordering': ('title',), 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='GalleryCategory', 38 | fields=[ 39 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('name', models.CharField(max_length=256, verbose_name='Name')), 41 | ('slug', models.SlugField(max_length=32, verbose_name='Slug')), 42 | ], 43 | options={ 44 | 'verbose_name': 'Gallery Category', 45 | 'verbose_name_plural': 'Gallery Categories', 46 | 'ordering': ('slug',), 47 | }, 48 | ), 49 | migrations.CreateModel( 50 | name='GalleryPlugin', 51 | fields=[ 52 | ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='image_gallery_galleryplugin', serialize=False, to='cms.CMSPlugin')), 53 | ('display_type', models.CharField(blank=True, choices=[('default', 'Default'), ('teaser', 'Teaser')], max_length=256)), 54 | ('gallery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='image_gallery.Gallery', verbose_name='Gallery')), 55 | ], 56 | options={ 57 | 'abstract': False, 58 | }, 59 | bases=('cms.cmsplugin',), 60 | ), 61 | migrations.CreateModel( 62 | name='GalleryImageExtension', 63 | fields=[ 64 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 65 | ('is_featured_image', models.BooleanField(default=False, verbose_name='Is featured image')), 66 | ('image', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.FILER_IMAGE_MODEL, verbose_name='Image')), 67 | ], 68 | options={ 69 | 'verbose_name': 'Gallery Image Extension', 70 | 'verbose_name_plural': 'Gallery Image Extensions', 71 | }, 72 | ), 73 | migrations.AddField( 74 | model_name='gallery', 75 | name='category', 76 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='image_gallery.GalleryCategory', verbose_name='Category'), 77 | ), 78 | migrations.AddField( 79 | model_name='gallery', 80 | name='description', 81 | field=cms.models.fields.PlaceholderField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, slotname='description', to='cms.Placeholder', verbose_name='Description'), 82 | ), 83 | migrations.AddField( 84 | model_name='gallery', 85 | name='folder', 86 | field=filer.fields.folder.FilerFolderField(on_delete=django.db.models.deletion.CASCADE, to='filer.Folder', verbose_name='Folder'), 87 | ), 88 | ] 89 | -------------------------------------------------------------------------------- /image_gallery/models.py: -------------------------------------------------------------------------------- 1 | """Models for the ``image_gallery`` app.""" 2 | from itertools import chain 3 | 4 | from django.db import models 5 | from django.urls import reverse 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from cms.models import CMSPlugin 9 | from cms.models.fields import PlaceholderField 10 | from filer.fields.folder import FilerFolderField 11 | from filer.models.imagemodels import Image 12 | 13 | from . import app_settings 14 | 15 | 16 | class Gallery(models.Model): 17 | """ 18 | Model to display a filer folder's contents and provide extra information. 19 | 20 | :title: Gallery title. 21 | :date: Date/Time of the gallery event. 22 | :location: Location of the gallery items. 23 | :description: Description of the gallery. 24 | :folder: Linked folder of the filer app. 25 | :is_published: True if the Gallery is published or not. 26 | 27 | """ 28 | category = models.ForeignKey( 29 | 'image_gallery.GalleryCategory', 30 | verbose_name=_('Category'), 31 | blank=True, null=True, 32 | on_delete=models.SET_NULL, 33 | ) 34 | 35 | title = models.CharField( 36 | max_length=100, 37 | verbose_name=_('Title'), 38 | ) 39 | 40 | date = models.DateTimeField( 41 | verbose_name=_('Date'), 42 | blank=True, null=True, 43 | ) 44 | 45 | location = models.CharField( 46 | max_length=100, 47 | verbose_name=_('Location'), 48 | blank=True, null=True, 49 | ) 50 | 51 | description = PlaceholderField( 52 | 'description', 53 | verbose_name=_('Description'), 54 | ) 55 | 56 | folder = FilerFolderField( 57 | verbose_name=_('Folder'), 58 | on_delete=models.CASCADE, 59 | ) 60 | 61 | is_published = models.BooleanField( 62 | verbose_name=_('Is published'), 63 | default=False, 64 | ) 65 | 66 | class Meta: 67 | ordering = ('title', ) 68 | verbose_name = _('Gallery') 69 | verbose_name_plural = _('Galleries') 70 | 71 | def __str__(self): 72 | return self.title 73 | 74 | def get_absolute_url(self): 75 | return reverse('image_gallery_detail', kwargs={'pk': self.pk, }) 76 | 77 | def get_featured_images(self): 78 | """ 79 | Returns those images of a given Gallery that are featured images. 80 | 81 | TODO: Find a way to test this 82 | 83 | """ 84 | result = [] 85 | for image in self.get_folder_images(): 86 | try: 87 | if image.galleryimageextension.is_featured_image: 88 | result.append(image) 89 | except GalleryImageExtension.DoesNotExist: 90 | pass 91 | return result 92 | 93 | def get_folder_images(self): 94 | """ 95 | Returns a set of images, which have been placed in this folder. 96 | 97 | TODO: Find a way to test this 98 | 99 | """ 100 | qs_files = self.folder.files.instance_of(Image) 101 | return qs_files.filter(is_public=True) 102 | 103 | def get_folder_image_list(self): 104 | """ 105 | Returns a list of images, which have been placed in this folder. 106 | 107 | They are first sorted by name, followed by those without name, sorted 108 | by file name. 109 | 110 | """ 111 | qs_files = self.folder.files.instance_of(Image) 112 | qs_files = qs_files.filter(is_public=True) 113 | return list(chain( 114 | qs_files.exclude(name='').order_by('name'), 115 | qs_files.filter(name='').order_by('file'))) 116 | 117 | 118 | class GalleryCategory(models.Model): 119 | """ 120 | Is used to categorize galleries. 121 | 122 | :name: Then human readable name of the category. 123 | :slug: The slug of the category 124 | 125 | """ 126 | name = models.CharField( 127 | max_length=256, 128 | verbose_name=_('Name'), 129 | ) 130 | 131 | slug = models.SlugField( 132 | max_length=32, 133 | verbose_name=_('Slug'), 134 | ) 135 | 136 | def __str__(self): 137 | return self.name 138 | 139 | class Meta: 140 | ordering = ('slug', ) 141 | verbose_name = _('Gallery Category') 142 | verbose_name_plural = _('Gallery Categories') 143 | 144 | 145 | class GalleryImageExtension(models.Model): 146 | """ 147 | Adds extra fields to the FilerImage admin. 148 | 149 | :image: The Image instance this object is extending. 150 | :is_featured_image: A boolean field that can be used for example to render 151 | a teaser of a gallery and only show a few featured images. 152 | """ 153 | image = models.OneToOneField( 154 | Image, 155 | verbose_name=_('Image'), 156 | on_delete=models.CASCADE, 157 | ) 158 | 159 | is_featured_image = models.BooleanField( 160 | default=False, 161 | verbose_name=_('Is featured image'), 162 | ) 163 | 164 | class Meta: 165 | verbose_name = _('Gallery Image Extension') 166 | verbose_name_plural = _('Gallery Image Extensions') 167 | 168 | 169 | class GalleryPlugin(CMSPlugin): 170 | """ 171 | Plugin model to link to a specific gallery instance. 172 | 173 | :gallery: The gallery instance that this plugin should render 174 | :display_type: A string that will be passed to the plugin templates. This 175 | allows you to render the gallery differently at different places on your 176 | page. 177 | 178 | """ 179 | gallery = models.ForeignKey( 180 | Gallery, 181 | verbose_name=_('Gallery'), 182 | on_delete=models.CASCADE, 183 | ) 184 | 185 | display_type = models.CharField( 186 | max_length=256, 187 | choices=app_settings.DISPLAY_TYPE_CHOICES, 188 | blank=True, 189 | ) 190 | --------------------------------------------------------------------------------