├── 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 |
3 | {% for picture in pictures %}
4 | 
5 | {% endfor %}
6 |
--------------------------------------------------------------------------------
/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 |
6 | {% for image in gallery.get_folder_images %}
7 | 
8 | {% endfor %}
9 |
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 |
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 |
--------------------------------------------------------------------------------