├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── requirements-test.txt ├── requirements.txt ├── responsive_wrapper ├── __init__.py ├── cms_app.py ├── cms_plugins.py ├── conf.py ├── fields.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── south_migrations │ ├── 0001_initial.py │ └── __init__.py ├── static │ └── js │ │ └── responsive_wrapper │ │ └── jquery.responsive-wrapper.js ├── templates │ └── responsive_wrapper │ │ ├── default.html │ │ ├── live_reload.html │ │ └── render_plugin.html ├── urls.py ├── utils.py └── views.py ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_models.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.py] 16 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 17 | line_length=120 18 | known_first_party=blogitty 19 | multi_line_output=3 20 | default_section=THIRDPARTY 21 | 22 | [*.yml] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | [*.md] 27 | trim_trailing_whitespace = false 28 | 29 | [Makefile] 30 | indent_style = tab 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Complexity 38 | output/*.html 39 | output/*/index.html 40 | 41 | # Sphinx 42 | docs/_build 43 | 44 | #PyCharm 45 | .idea 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | python: 6 | - "3.3" 7 | - "2.7" 8 | - "2.6" 9 | 10 | env: 11 | - DJANGO="Django==1.5" 12 | - DJANGO="Django==1.6" 13 | - DJANGO="Django==1.7" 14 | 15 | matrix: 16 | exclude: 17 | # Python 2.6 support has been dropped in Django 1.7 18 | - python: "2.6" 19 | env: DJANGO="Django==1.7" 20 | 21 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 22 | install: 23 | - pip install $DJANGO 24 | - pip install -r requirements-test.txt 25 | 26 | # command to run tests using coverage, e.g. python setup.py test 27 | script: coverage run --source responsive_wrapper runtests.py 28 | 29 | # report coverage to coveralls.io 30 | after_success: coveralls -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Mishbah Razzaque 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/mishbahr/djangocms-responsive-wrapper/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | djangocms-responsive-wrapper could always use more documentation, whether as part of the 40 | official djangocms-responsive-wrapper docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/mishbahr/djangocms-responsive-wrapper/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `djangocms-responsive-wrapper` for local development. 59 | 60 | 1. Fork the `djangocms-responsive-wrapper` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/djangocms-responsive-wrapper.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv djangocms-responsive-wrapper 68 | $ cd djangocms-responsive-wrapper/ 69 | $ python setup.py develop 70 | 71 | 4. Create a branch for local development:: 72 | 73 | $ git checkout -b name-of-your-bugfix-or-feature 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. When you're done making changes, check that your changes pass flake8 and the 78 | tests, including testing other Python versions with tox:: 79 | 80 | $ flake8 responsive_wrapper tests 81 | $ python setup.py test 82 | $ tox 83 | 84 | To get flake8 and tox, just pip install them into your virtualenv. 85 | 86 | 6. Commit your changes and push your branch to GitHub:: 87 | 88 | $ git add . 89 | $ git commit -m "Your detailed description of your changes." 90 | $ git push origin name-of-your-bugfix-or-feature 91 | 92 | 7. Submit a pull request through the GitHub website. 93 | 94 | Pull Request Guidelines 95 | ----------------------- 96 | 97 | Before you submit a pull request, check that it meets these guidelines: 98 | 99 | 1. The pull request should include tests. 100 | 2. If the pull request adds functionality, the docs should be updated. Put 101 | your new functionality into a function with a docstring, and add the 102 | feature to the list in README.rst. 103 | 3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check 104 | https://travis-ci.org/mishbahr/djangocms-responsive-wrapper/pull_requests 105 | and make sure that the tests pass for all supported Python versions. 106 | 107 | Tips 108 | ---- 109 | 110 | To run a subset of tests:: 111 | 112 | $ python -m unittest tests.test_responsive_wrapper -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.1.0 (2014-12-05) 7 | ++++++++++++++++++ 8 | 9 | * First release on PyPI. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Mishbah Razzaque 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of djangocms-responsive-wrapper nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | recursive-include responsive_wrapper *.html *.png *.gif *js *.css *jpg *jpeg *svg *py -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs 2 | 3 | help: 4 | @echo "clean-build - remove build artifacts" 5 | @echo "clean-pyc - remove Python file artifacts" 6 | @echo "lint - check style with flake8" 7 | @echo "test - run tests quickly with the default Python" 8 | @echo "testall - run tests on every Python version with tox" 9 | @echo "coverage - check code coverage quickly with the default Python" 10 | @echo "docs - generate Sphinx HTML documentation, including API docs" 11 | @echo "release - package and upload a release" 12 | @echo "sdist - package" 13 | 14 | clean: clean-build clean-pyc 15 | 16 | clean-build: 17 | rm -fr build/ 18 | rm -fr dist/ 19 | rm -fr *.egg-info 20 | 21 | clean-pyc: 22 | find . -name '*.pyc' -exec rm -f {} + 23 | find . -name '*.pyo' -exec rm -f {} + 24 | find . -name '*~' -exec rm -f {} + 25 | 26 | lint: 27 | flake8 djangocms-responsive-wrapper tests 28 | 29 | test: 30 | python runtests.py test 31 | 32 | test-all: 33 | tox 34 | 35 | coverage: 36 | coverage run --source djangocms-responsive-wrapper setup.py test 37 | coverage report -m 38 | coverage html 39 | open htmlcov/index.html 40 | 41 | docs: 42 | rm -f docs/djangocms-responsive-wrapper.rst 43 | rm -f docs/modules.rst 44 | sphinx-apidoc -o docs/ djangocms-responsive-wrapper 45 | $(MAKE) -C docs clean 46 | $(MAKE) -C docs html 47 | open docs/_build/html/index.html 48 | 49 | release: clean 50 | python setup.py sdist upload 51 | python setup.py bdist_wheel upload 52 | 53 | sdist: clean 54 | python setup.py sdist 55 | ls -l dist -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | djangocms-responsive-wrapper 3 | ============================= 4 | 5 | .. image:: http://img.shields.io/pypi/v/djangocms-responsive-wrapper.svg?style=flat-square 6 | :target: https://pypi.python.org/pypi/djangocms-responsive-wrapper/ 7 | :alt: Latest Version 8 | 9 | .. image:: http://img.shields.io/pypi/dm/djangocms-responsive-wrapper.svg?style=flat-square 10 | :target: https://pypi.python.org/pypi/djangocms-responsive-wrapper/ 11 | :alt: Downloads 12 | 13 | .. image:: http://img.shields.io/pypi/l/djangocms-responsive-wrapper.svg?style=flat-square 14 | :target: https://pypi.python.org/pypi/djangocms-responsive-wrapper/ 15 | :alt: License 16 | 17 | 18 | This projects integrates `django-responsive2 `_ with `django-cms >= 3.0 `_ 19 | 20 | This django CMS plugin will allow a site editor to display different contents based on breakpoints. For a more detailed description and reasoning behind this concept, please read the project description for ``django-responsive2.`` 21 | 22 | This project requires ``django-responsive2`` and ``django-cms`` v3.0 or higher to be properly installed and configured. When installing the ``djangocms-responsive-wrapper`` using pip, ``django-responsive2`` will also be installed automatically. 23 | 24 | The full documentation for ``django-responsive2`` is available at https://django-responsive2.readthedocs.org. 25 | 26 | 27 | 28 | Quickstart 29 | ---------- 30 | 31 | 1. Install ``djangocms-responsive-wrapper``:: 32 | 33 | pip install djangocms-responsive-wrapper 34 | 35 | 2. Add ``responsive_wrapper`` to ``INSTALLED_APPS``:: 36 | 37 | INSTALLED_APPS = ( 38 | ... 39 | 'responsive_wrapper', 40 | ... 41 | ) 42 | 43 | AJAX load plugin based on window size 44 | ------------------------------------- 45 | By default, ``responsive_wrapper`` renders the plugins based on device dimensions. 46 | 47 | However, if you would like the plugin to use the window size to render the plugin, set the ``RESPONSIVE_WRAPPER_TEMPLATE`` to use an alternate template:: 48 | 49 | RESPONSIVE_WRAPPER_TEMPLATE = 'responsive_wrapper/live_reload.html' 50 | 51 | And add the ``responsive_wrapper.urls`` to your project's ``urls`` module or create a django CMS page to hook the application into. In ``Advanced Settings``, set its Application to ``Responsive Wrapper`` (this requires a server restart):: 52 | 53 | urlpatterns = patterns( 54 | ... 55 | url(r'^responsive/', include('responsive_wrapper.urls')), 56 | ... 57 | ) 58 | 59 | The ``ResponsiveWrapper.js``, included in the ``live_reload.html`` triggers a ``replace`` event when the content has been replaced. This can be useful when you want to change some styles or reinitialize any JavaScript on your page based on which content is loaded. 60 | 61 | Configuration 62 | ------------- 63 | 64 | Plugin(s) Module:: 65 | 66 | RESPONSIVE_WRAPPER_MODULE = _('Generic') 67 | 68 | Name of the plugin:: 69 | 70 | RESPONSIVE_WRAPPER_NAME = _('Responsive Wrapper') 71 | 72 | The path to the template used to render the template:: 73 | 74 | RESPONSIVE_WRAPPER_TEMPLATE = 'responsive_wrapper/default.html' 75 | 76 | Can the plugin be inserted inside the text plugin? 77 | :: 78 | 79 | RESPONSIVE_WRAPPER_TEXT_ENABLED = False 80 | 81 | Can this plugin only be attached to a placeholder that is attached to a page?:: 82 | 83 | RESPONSIVE_WRAPPER_PAGE_ONLY = False 84 | 85 | A List of Plugin Class Names. If this is set, only plugins listed here can be added to this plugin:: 86 | 87 | RESPONSIVE_WRAPPER_CHILD_CLASSES = None 88 | 89 | Is it required that this plugin is a child of another plugin? Or can it be added to any placeholder:: 90 | 91 | RESPONSIVE_WRAPPER_REQUIRE_PARENT = False 92 | 93 | A list of Plugin Class Names. If this is set, this plugin may only be added to plugins listed here:: 94 | 95 | RESPONSIVE_WRAPPER_PARENT_CLASSES = None 96 | 97 | Set fieldsets to control the layout of plugin “add” and “change” form:: 98 | 99 | RESPONSIVE_WRAPPER_FIELDSETS = None 100 | 101 | 102 | You may also like... 103 | -------------------- 104 | 105 | * djangocms-disqus - https://github.com/mishbahr/djangocms-disqus 106 | * djangocms-fbcomments - https://github.com/mishbahr/djangocms-fbcomments 107 | * djangocms-forms — https://github.com/mishbahr/djangocms-forms 108 | * djangocms-gmaps — https://github.com/mishbahr/djangocms-gmaps 109 | * djangocms-instagram — https://github.com/mishbahr/djangocms-instagram 110 | * djangocms-twitter2 — https://github.com/mishbahr/djangocms-twitter2 111 | * djangocms-youtube — https://github.com/mishbahr/djangocms-youtube 112 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | coverage 4 | coveralls 5 | mock>=1.0.1 6 | nose>=1.3.0 7 | django-nose>=1.2 8 | flake8>=2.1.0 9 | tox>=1.7.0 10 | 11 | # Additional test requirements go here 12 | south==1.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | wheel==0.24.0 3 | 4 | # Additional requirements go here -------------------------------------------------------------------------------- /responsive_wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.1' 2 | -------------------------------------------------------------------------------- /responsive_wrapper/cms_app.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from cms.app_base import CMSApp 6 | from cms.apphook_pool import apphook_pool 7 | 8 | 9 | class ResponsiveWrapperApphook(CMSApp): 10 | name = _('Responsive Wrapper') 11 | urls = ['responsive_wrapper.urls'] 12 | 13 | apphook_pool.register(ResponsiveWrapperApphook) 14 | -------------------------------------------------------------------------------- /responsive_wrapper/cms_plugins.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.utils import six 4 | from django.utils.safestring import mark_safe 5 | 6 | from cms.plugin_base import CMSPluginBase 7 | from cms.plugin_pool import plugin_pool 8 | 9 | from responsive.utils import Device 10 | 11 | from .conf import settings 12 | from .forms import ResponsiveWrapperForm 13 | from .models import ResponsiveWrapper 14 | 15 | 16 | class ResponsiveWrapperPlugin(CMSPluginBase): 17 | model = ResponsiveWrapper 18 | form = ResponsiveWrapperForm 19 | allow_children = True 20 | cache = False 21 | 22 | render_template = settings.RESPONSIVE_WRAPPER_TEMPLATE 23 | name = settings.RESPONSIVE_WRAPPER_NAME 24 | module = settings.RESPONSIVE_WRAPPER_MODULE 25 | text_enabled = settings.RESPONSIVE_WRAPPER_TEXT_ENABLED 26 | page_only = settings.RESPONSIVE_WRAPPER_PAGE_ONLY 27 | child_classes = settings.RESPONSIVE_WRAPPER_CHILD_CLASSES 28 | require_parent = settings.RESPONSIVE_WRAPPER_REQUIRE_PARENT 29 | parent_classes = settings.RESPONSIVE_WRAPPER_PARENT_CLASSES 30 | 31 | def render(self, context, instance, placeholder): 32 | context = super(ResponsiveWrapperPlugin, self).render(context, instance, placeholder) 33 | request = context.get('request', None) 34 | 35 | device = getattr(request, settings.RESPONSIVE_VARIABLE_NAME, None) 36 | if not device: 37 | raise ImproperlyConfigured( 38 | "You must enable the 'ResponsiveMiddleware'. Edit your " 39 | "MIDDLEWARE_CLASSES setting to insert" 40 | "the 'responsive.middleware.ResponsiveMiddleware'") 41 | 42 | if request.is_ajax(): 43 | device_info = { 44 | 'width': request.GET.get('width', device.width), 45 | 'height': request.GET.get('width', device.height), 46 | 'pixel_ratio': request.GET.get('dpr', device.pixel_ratio), 47 | } 48 | device = Device(**device_info) 49 | 50 | context[settings.RESPONSIVE_VARIABLE_NAME] = device 51 | 52 | matched_media_query = False 53 | for name, value in six.iteritems(instance.media_queries): 54 | if value is True and name in device.matched: 55 | matched_media_query = True 56 | 57 | context['matched_media_query'] = matched_media_query 58 | return context 59 | 60 | def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): 61 | form = context['adminform'].form 62 | adminform = admin.helpers.AdminForm( 63 | form, 64 | settings.RESPONSIVE_WRAPPER_FIELDSETS or [(None, {'fields': form.fields.keys()})], 65 | self.prepopulated_fields 66 | ) 67 | media = mark_safe(self.media + adminform.media) 68 | context.update(adminform=adminform, media=media) 69 | return super(ResponsiveWrapperPlugin, self).render_change_form( 70 | request, context, add, change, form_url, obj) 71 | 72 | 73 | plugin_pool.register_plugin(ResponsiveWrapperPlugin) 74 | -------------------------------------------------------------------------------- /responsive_wrapper/conf.py: -------------------------------------------------------------------------------- 1 | from appconf import AppConf 2 | 3 | from django.conf import settings # noqa 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class ResponsiveWrapperConf(AppConf): 8 | CHILD_CLASSES = None 9 | FIELDSETS = None 10 | MODULE = _('Generic') 11 | NAME = _('Responsive Wrapper') 12 | PAGE_ONLY = False 13 | PARENT_CLASSES = None 14 | REQUIRE_PARENT = False 15 | TEMPLATE = 'responsive_wrapper/default.html' 16 | TEXT_ENABLED = False 17 | 18 | HASHIDS_SALT = settings.SECRET_KEY 19 | 20 | class Meta: 21 | prefix = 'responsive_wrapper' 22 | -------------------------------------------------------------------------------- /responsive_wrapper/fields.py: -------------------------------------------------------------------------------- 1 | from django.core.serializers.json import DjangoJSONEncoder 2 | from django.db import models 3 | from django.utils import six 4 | 5 | # South support. 6 | try: 7 | from south.modelsinspector import add_introspection_rules 8 | SOUTH = True 9 | except ImportError: 10 | SOUTH = False 11 | 12 | # Try to be compatible with Django 1.5+. 13 | try: 14 | import json 15 | except ImportError: 16 | from django.utils import simplejson as json 17 | 18 | # Basestring no longer exists in Python 3 19 | try: 20 | basestring 21 | except NameError: 22 | basestring = str 23 | 24 | 25 | class JSONField(six.with_metaclass(models.SubfieldBase, models.TextField)): 26 | 27 | def to_python(self, value): 28 | if value == '': 29 | return None 30 | 31 | try: 32 | if isinstance(value, basestring): 33 | return json.loads(value) 34 | elif isinstance(value, bytes): 35 | return json.loads(value.decode('utf8')) 36 | except ValueError: 37 | pass 38 | return value 39 | 40 | def get_db_prep_save(self, value, *args, **kwargs): 41 | if value == '': 42 | return None 43 | if isinstance(value, dict) or isinstance(value, list): 44 | value = json.dumps(value, cls=DjangoJSONEncoder) 45 | return super(JSONField, self).get_db_prep_save(value, *args, **kwargs) 46 | 47 | def value_from_object(self, obj): 48 | value = super(JSONField, self).value_from_object(obj) 49 | if self.null and value is None: 50 | return None 51 | return json.dumps(value) 52 | 53 | if SOUTH: 54 | add_introspection_rules([], ['^responsive_wrapper.fields.JSONField']) 55 | -------------------------------------------------------------------------------- /responsive_wrapper/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils import six 3 | 4 | from .conf import settings 5 | from .models import ResponsiveWrapper 6 | 7 | 8 | class ResponsiveWrapperForm(forms.ModelForm): 9 | 10 | def __init__(self, *args, **kwargs): 11 | super(ResponsiveWrapperForm, self).__init__(*args, **kwargs) 12 | 13 | for name, config in six.iteritems(settings.RESPONSIVE_MEDIA_QUERIES): 14 | label = config.get('verbose_name', name.replace('_', ' ').title()) 15 | help_text = config.get('help_text', '') 16 | self.fields[name] = forms.BooleanField( 17 | label=label, help_text=help_text, initial=True, required=False) 18 | 19 | if bool(self.instance.pk): 20 | self.initial.update(self.instance.media_queries) 21 | 22 | def save(self, commit=True): 23 | instance = super(ResponsiveWrapperForm, self).save(commit=False) 24 | instance.media_queries = self.cleaned_data 25 | if commit: 26 | instance.save() 27 | return instance 28 | 29 | class Meta: 30 | model = ResponsiveWrapper 31 | -------------------------------------------------------------------------------- /responsive_wrapper/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import responsive_wrapper.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cms', '__first__'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ResponsiveWrapper', 17 | fields=[ 18 | ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')), 19 | ('media_queries', responsive_wrapper.fields.JSONField(editable=False)), 20 | ], 21 | options={ 22 | 'abstract': False, 23 | }, 24 | bases=('cms.cmsplugin',), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /responsive_wrapper/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django migrations for responsive_wrapper app 3 | 4 | This package does not contain South migrations. South migrations can be found 5 | in the ``south_migrations`` package. 6 | """ 7 | 8 | SOUTH_ERROR_MESSAGE = """\n 9 | For South support, customize the SOUTH_MIGRATION_MODULES setting like so: 10 | 11 | SOUTH_MIGRATION_MODULES = { 12 | 'responsive_wrapper': 'responsive_wrapper.south_migrations', 13 | } 14 | """ 15 | 16 | # Ensure the user is not using Django 1.6 or below with South 17 | try: 18 | from django.db import migrations # noqa 19 | except ImportError: 20 | from django.core.exceptions import ImproperlyConfigured 21 | raise ImproperlyConfigured(SOUTH_ERROR_MESSAGE) 22 | -------------------------------------------------------------------------------- /responsive_wrapper/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.utils import six 3 | from django.utils.encoding import force_text, python_2_unicode_compatible 4 | 5 | from cms.models.pluginmodel import CMSPlugin 6 | 7 | from .conf import settings 8 | from .fields import JSONField 9 | from .utils import int_to_hashid 10 | 11 | 12 | @python_2_unicode_compatible 13 | class ResponsiveWrapper(CMSPlugin): 14 | media_queries = JSONField(editable=False) 15 | 16 | def __str__(self): 17 | media_queries = [] 18 | for name, value in six.iteritems(self.media_queries): 19 | if value: 20 | try: 21 | config = settings.RESPONSIVE_MEDIA_QUERIES[name] 22 | except KeyError: 23 | pass 24 | else: 25 | media_queries.append( 26 | force_text(config.get('verbose_name', name.replace('_', ' ').title())) 27 | ) 28 | if len(media_queries) > 1: 29 | return u'Visible on {0} and {1}.'.format( 30 | ', '.join(media_queries[:-1]), media_queries[-1]) 31 | elif media_queries: 32 | return u'Visible on {0}.'.format(media_queries[0]) 33 | 34 | @property 35 | def hash_id(self): 36 | if self.pk: 37 | return int_to_hashid(self.pk) 38 | return '' 39 | -------------------------------------------------------------------------------- /responsive_wrapper/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'ResponsiveWrapper' 12 | db.create_table(u'responsive_wrapper_responsivewrapper', ( 13 | (u'cmsplugin_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.CMSPlugin'], unique=True, primary_key=True)), 14 | ('media_queries', self.gf('responsive_wrapper.fields.JSONField')()), 15 | )) 16 | db.send_create_signal(u'responsive_wrapper', ['ResponsiveWrapper']) 17 | 18 | 19 | def backwards(self, orm): 20 | # Deleting model 'ResponsiveWrapper' 21 | db.delete_table(u'responsive_wrapper_responsivewrapper') 22 | 23 | 24 | models = { 25 | 'cms.cmsplugin': { 26 | 'Meta': {'object_name': 'CMSPlugin'}, 27 | 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 28 | 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 29 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), 31 | 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 32 | 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 33 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}), 34 | 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}), 35 | 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), 36 | 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}), 37 | 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 38 | 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) 39 | }, 40 | 'cms.placeholder': { 41 | 'Meta': {'object_name': 'Placeholder'}, 42 | 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), 43 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 44 | 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) 45 | }, 46 | u'responsive_wrapper.responsivewrapper': { 47 | 'Meta': {'object_name': 'ResponsiveWrapper', '_ormbases': ['cms.CMSPlugin']}, 48 | u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'}), 49 | 'media_queries': ('responsive_wrapper.fields.JSONField', [], {}) 50 | } 51 | } 52 | 53 | complete_apps = ['responsive_wrapper'] -------------------------------------------------------------------------------- /responsive_wrapper/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mishbahr/djangocms-responsive-wrapper/d12c1d6830fab204f812b00de3be46e14c101a5d/responsive_wrapper/south_migrations/__init__.py -------------------------------------------------------------------------------- /responsive_wrapper/static/js/responsive_wrapper/jquery.responsive-wrapper.js: -------------------------------------------------------------------------------- 1 | // the semi-colon before function invocation is a safety net against concatenated 2 | // scripts and/or other plugins which may not be closed properly. 3 | ;(function($, window, document, undefined) { 4 | 5 | var render = 'render'; 6 | 7 | function ResponsiveWrapper(el, options) { 8 | this.el = $(el); 9 | this.settings = $.extend({}, defaults, options); 10 | this._defaults = defaults; 11 | this._name = render; 12 | this.ajaxUrl = this.el.data('ajaxUrl'); 13 | this.init(); 14 | } 15 | 16 | ResponsiveWrapper.prototype = { 17 | init: function() { 18 | $(window).on('resize load', this.debounce(function() { 19 | this.onResize(); 20 | }.bind(this), 300)); 21 | }, 22 | debounce : function (func, delay, immediate) { 23 | var timeout, result; 24 | return function () { 25 | var context = this, args = arguments; 26 | var later = function () { 27 | timeout = null; 28 | if (!immediate) { 29 | result = func.apply(context, args); 30 | } 31 | }; 32 | var callNow = immediate && !timeout; 33 | clearTimeout(timeout); 34 | timeout = setTimeout(later, delay); 35 | if (callNow) { 36 | result = func.apply(context, args); 37 | } 38 | return result; 39 | }; 40 | }, 41 | onResize: function() { 42 | var that = this; 43 | $.get(this.ajaxUrl, { 44 | width: $(window).width(), 45 | height: $(window).height(), 46 | dpr: window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI) || 1 47 | }, function(response) { 48 | that.el.html(response); 49 | that.el.trigger('replace'); 50 | }); 51 | } 52 | }; 53 | 54 | $.fn[render] = function(options) { 55 | return this.each(function() { 56 | if (!$.data(this, 'plugin_' + render)) { 57 | $.data(this, 'plugin_' + render, new ResponsiveWrapper(this, options)); 58 | } 59 | }); 60 | }; 61 | 62 | })(jQuery, window, document); 63 | -------------------------------------------------------------------------------- /responsive_wrapper/templates/responsive_wrapper/default.html: -------------------------------------------------------------------------------- 1 | {% load cms_tags %} 2 | 3 | {% if matched_media_query %} 4 | {% for plugin in instance.child_plugin_instances %} 5 | {% render_plugin plugin %} 6 | {% endfor %} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /responsive_wrapper/templates/responsive_wrapper/live_reload.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles cms_tags sekizai_tags %} 2 | 3 | {% if not request.is_ajax %} 4 |
5 | {% for plugin in instance.child_plugin_instances %} 6 | {% render_plugin plugin %} 7 | {% endfor %} 8 |
9 | 10 | {% addtoblock 'js' %} 11 | 12 | {% endaddtoblock %} 13 | 14 | {% addtoblock 'js' %} 15 | 18 | {% endaddtoblock %} 19 | {% else %} 20 | {% if matched_media_query %} 21 | {% for plugin in instance.child_plugin_instances %} 22 | {% render_plugin plugin %} 23 | {% endfor %} 24 | {% endif %} 25 | {% endif %} 26 | 27 | -------------------------------------------------------------------------------- /responsive_wrapper/templates/responsive_wrapper/render_plugin.html: -------------------------------------------------------------------------------- 1 | {% load cms_tags %}{% for plugin in plugins %}{% render_plugin plugin %}{% endfor %} 2 | -------------------------------------------------------------------------------- /responsive_wrapper/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from .views import render_plugin 4 | 5 | urlpatterns = patterns( 6 | '', 7 | url(r'^render/(?P[-\w]+)/$', render_plugin, name='render-plugin') 8 | ) 9 | -------------------------------------------------------------------------------- /responsive_wrapper/utils.py: -------------------------------------------------------------------------------- 1 | from hashids import Hashids 2 | 3 | from .conf import settings 4 | 5 | 6 | def int_to_hashid(i, min_length=11, salt=settings.RESPONSIVE_WRAPPER_HASHIDS_SALT): 7 | hashids = Hashids(salt, min_length=min_length) 8 | return hashids.encode(i) 9 | 10 | 11 | def hashid_to_int(hashid, min_length=11, salt=settings.RESPONSIVE_WRAPPER_HASHIDS_SALT): 12 | hashids = Hashids(salt, min_length=min_length) 13 | 14 | try: 15 | return hashids.decode(hashid)[0] 16 | except IndexError: 17 | pass 18 | -------------------------------------------------------------------------------- /responsive_wrapper/views.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import FieldError 2 | from django.http import HttpResponseNotFound 3 | from django.shortcuts import render_to_response 4 | from django.template import RequestContext 5 | from django.utils.encoding import force_text 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from cms.models import CMSPlugin 9 | from cms.utils.plugins import build_plugin_tree, downcast_plugins 10 | 11 | from .utils import hashid_to_int 12 | 13 | 14 | def render_plugin(request, plugin_id, template_name='responsive_wrapper/render_plugin.html'): 15 | try: 16 | instance = CMSPlugin.objects.get(pk=hashid_to_int(plugin_id)) 17 | except CMSPlugin.DoesNotExist: 18 | msg = _('Plugin not found.') 19 | return HttpResponseNotFound(force_text(msg)) 20 | 21 | try: 22 | # django CMS 3- 23 | descendants = instance.get_descendants(include_self=True) \ 24 | .order_by('placeholder', 'tree_id', 'level', 'position') 25 | except (FieldError, TypeError): 26 | # django CMS 3.1+ 27 | descendants = instance.get_descendants().order_by('path') 28 | 29 | plugins = [instance] + list(descendants) 30 | plugins = downcast_plugins(plugins) 31 | plugins[0].parent_id = None 32 | plugins = build_plugin_tree(plugins) 33 | 34 | context = RequestContext(request) 35 | context['plugins'] = plugins 36 | return render_to_response(template_name, {}, context) 37 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import django 4 | 5 | try: 6 | from django.conf import settings 7 | 8 | _settings = { 9 | 'DEBUG': True, 10 | 'USE_TZ': True, 11 | 'LANGUAGE_CODE': 'en-us', 12 | 'LANGUAGES': ( 13 | ('en-us', 'English'), 14 | ), 15 | 'DATABASES': { 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | 'NAME': 'responsive', 19 | } 20 | }, 21 | 'ROOT_URLCONF': 'responsive_wrapper.urls', 22 | 'INSTALLED_APPS': [ 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.sites', 27 | 'django.contrib.messages', 28 | 'django.contrib.admin', 29 | 'cms', 30 | 'mptt', 31 | 'menus', 32 | 'sekizai', 33 | 'responsive', 34 | 'responsive_wrapper', 35 | ], 36 | 'MIDDLEWARE_CLASSES': ( 37 | 'django.middleware.common.CommonMiddleware', 38 | 'django.contrib.sessions.middleware.SessionMiddleware', 39 | 'django.middleware.csrf.CsrfViewMiddleware', 40 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 41 | 'django.contrib.messages.middleware.MessageMiddleware', 42 | 43 | # django-cms 44 | 'cms.middleware.page.CurrentPageMiddleware', 45 | 'cms.middleware.user.CurrentUserMiddleware', 46 | 'cms.middleware.toolbar.ToolbarMiddleware', 47 | 48 | 'responsive.middleware.ResponsiveMiddleware', # django-responsive2 49 | ), 50 | 'TEMPLATE_CONTEXT_PROCESSORS': ( 51 | 'django.contrib.auth.context_processors.auth', 52 | 'django.core.context_processors.i18n', 53 | 'django.core.context_processors.request', 54 | 'django.core.context_processors.media', 55 | 'django.core.context_processors.static', 56 | 'cms.context_processors.cms_settings', 57 | 'sekizai.context_processors.sekizai', 58 | 'responsive.context_processors.device', # django-responsive2 59 | ), 60 | 'SITE_ID': 1, 61 | 'SOUTH_MIGRATION_MODULES': { 62 | 'responsive_wrapper': 'responsive_wrapper.south_migrations', 63 | }, 64 | 'NOSE_ARGS': ['-s'], 65 | } 66 | 67 | 68 | if django.VERSION < (1,7): 69 | _settings['INSTALLED_APPS'] += ['south'] 70 | 71 | settings.configure(**_settings) 72 | 73 | try: 74 | setup = django.setup 75 | except AttributeError: 76 | pass 77 | else: 78 | setup() 79 | 80 | from django_nose import NoseTestSuiteRunner 81 | except ImportError: 82 | import traceback 83 | traceback.print_exc() 84 | raise ImportError("To fix this error, run: pip install -r requirements-test.txt") 85 | 86 | 87 | def run_tests(*test_args): 88 | if not test_args: 89 | test_args = ['tests'] 90 | 91 | # Run tests 92 | test_runner = NoseTestSuiteRunner(verbosity=1) 93 | 94 | failures = test_runner.run_tests(test_args) 95 | 96 | if failures: 97 | sys.exit(failures) 98 | 99 | 100 | if __name__ == '__main__': 101 | run_tests(*sys.argv[1:]) 102 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | #ignore = E265 6 | max-line-length = 120 7 | exclude = .tox,.git,*/migrations/*,*/south_migrations/*, */static/CACHE/*,docs -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | import responsive_wrapper 8 | 9 | try: 10 | from setuptools import setup 11 | except ImportError: 12 | from distutils.core import setup 13 | 14 | version = responsive_wrapper.__version__ 15 | 16 | if sys.argv[-1] == 'publish': 17 | os.system('python setup.py sdist upload') 18 | print("You probably want to also tag the version now:") 19 | print(" git tag -a %s -m 'version %s'" % (version, version)) 20 | print(" git push --tags") 21 | sys.exit() 22 | 23 | readme = open('README.rst').read() 24 | 25 | setup( 26 | name='djangocms-responsive-wrapper', 27 | version=version, 28 | description="""This projects integrates django-responsive2 with django-cms >= 3.0""", 29 | long_description=readme, 30 | author='Mishbah Razzaque', 31 | author_email='mishbahx@gmail.com', 32 | url='https://github.com/mishbahr/djangocms-responsive-wrapper', 33 | packages=[ 34 | 'responsive_wrapper', 35 | ], 36 | include_package_data=True, 37 | install_requires=[ 38 | 'django-appconf', 39 | 'django-cms>=3.0', 40 | 'django-responsive2', 41 | 'hashids', 42 | ], 43 | license="BSD", 44 | zip_safe=False, 45 | keywords='djangocms-responsive-wrapper, django-responsive2, django-cms', 46 | classifiers=[ 47 | 'Development Status :: 4 - Beta', 48 | 'Framework :: Django', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: BSD License', 51 | 'Natural Language :: English', 52 | 'Programming Language :: Python :: 2.7', 53 | 'Programming Language :: Python :: 3', 54 | 'Programming Language :: Python :: 3.3', 55 | ], 56 | ) 57 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mishbahr/djangocms-responsive-wrapper/d12c1d6830fab204f812b00de3be46e14c101a5d/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_djangocms-responsive-wrapper 6 | ------------ 7 | 8 | Tests for `djangocms-responsive-wrapper` models module. 9 | """ 10 | 11 | from django.conf import settings 12 | from django.test import TestCase 13 | 14 | from responsive_wrapper import models 15 | 16 | 17 | class TestResponsive_wrapper(TestCase): 18 | 19 | def setUp(self): 20 | pass 21 | 22 | def test_something(self): 23 | pass 24 | 25 | def tearDown(self): 26 | pass -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py33 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/responsive_wrapper 7 | commands = python runtests.py 8 | deps = 9 | -r{toxinidir}/requirements-test.txt --------------------------------------------------------------------------------