Full Code of jrief/djangocms-cascade for AI

master 6e4d5ec7d5cb cached
316 files
1.0 MB
260.2k tokens
866 symbols
1 requests
Download .txt
Showing preview only (1,133K chars total). Download the full file or copy to clipboard to get everything.
Repository: jrief/djangocms-cascade
Branch: master
Commit: 6e4d5ec7d5cb
Files: 316
Total size: 1.0 MB

Directory structure:
gitextract_301603hg/

├── .coveragerc
├── .editorconfig
├── .github/
│   └── workflows/
│       ├── publish.yml
│       └── tests.yml
├── .gitignore
├── BOOSTRAP-4.md
├── LICENSE-MIT
├── MANIFEST.in
├── README.md
├── cmsplugin_cascade/
│   ├── __init__.py
│   ├── admin.py
│   ├── app_settings.py
│   ├── apps.py
│   ├── bootstrap4/
│   │   ├── __init__.py
│   │   ├── accordion.py
│   │   ├── buttons.py
│   │   ├── card.py
│   │   ├── carousel.py
│   │   ├── container.py
│   │   ├── embeds.py
│   │   ├── fields.py
│   │   ├── grid.py
│   │   ├── icon.py
│   │   ├── image.py
│   │   ├── jumbotron.py
│   │   ├── mixins.py
│   │   ├── picture.py
│   │   ├── plugin_base.py
│   │   ├── secondary_menu.py
│   │   ├── settings.py
│   │   ├── tabs.py
│   │   └── utils.py
│   ├── clipboard/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── cms_plugins.py
│   │   ├── cms_toolbars.py
│   │   ├── forms.py
│   │   └── utils.py
│   ├── cms_plugins.py
│   ├── cms_toolbars.py
│   ├── extra_fields/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── config.py
│   │   └── mixins.py
│   ├── fields.py
│   ├── forms.py
│   ├── generic/
│   │   ├── __init__.py
│   │   ├── custom_snippet.py
│   │   ├── heading.py
│   │   ├── horizontal_rule.py
│   │   ├── mixins.py
│   │   ├── settings.py
│   │   ├── simple_wrapper.py
│   │   └── text_image.py
│   ├── hide_plugins.py
│   ├── icon/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── forms.py
│   │   ├── plugin_base.py
│   │   ├── settings.py
│   │   ├── simpleicon.py
│   │   ├── texticon.py
│   │   └── utils.py
│   ├── image.py
│   ├── leaflet/
│   │   ├── __init__.py
│   │   ├── map.py
│   │   └── settings.py
│   ├── link/
│   │   ├── __init__.py
│   │   ├── cms_plugins.py
│   │   ├── config.py
│   │   ├── forms.py
│   │   └── plugin_base.py
│   ├── locale/
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   └── uk/
│   │       └── LC_MESSAGES/
│   │           └── django.po
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_auto_20150530_1018.py
│   │   ├── 0003_inlinecascadeelement.py
│   │   ├── 0004_auto_20151112_0147.py
│   │   ├── 0005_tabset_and_clipboard.py
│   │   ├── 0006_bootstrapgallerypluginmodel.py
│   │   ├── 0007_add_proxy_models.py
│   │   ├── 0008_sortableinlinecascadeelement.py
│   │   ├── 0009_cascadepage.py
│   │   ├── 0010_refactor_heading.py
│   │   ├── 0011_merge_sharable_with_cascadeelement.py
│   │   ├── 0012_auto_20160619_1854.py
│   │   ├── 0013_iconfont.py
│   │   ├── 0014_glossary_field.py
│   │   ├── 0015_carousel_slide.py
│   │   ├── 0016_shared_glossary_uneditable.py
│   │   ├── 0017_fake_proxy_models.py
│   │   ├── 0018_iconfont_color.py
│   │   ├── 0019_verbose_table_names.py
│   │   ├── 0020_page_icon_font.py
│   │   ├── 0021_cascadepage_verbose_name.py
│   │   ├── 0022_auto_20181202_1055.py
│   │   ├── 0023_iconfont_is_default.py
│   │   ├── 0024_page_icon_font.py
│   │   ├── 0025_texteditorconfigfields.py
│   │   ├── 0026_cascadepage_menu_symbol.py
│   │   ├── 0027_version_1.py
│   │   ├── 0028_cascade_clipboard.py
│   │   ├── 0029_json_field.py
│   │   ├── 0030_separate_ids_per_language.py
│   │   ├── 0031_alter_texteditorconfigfields_element_type.py
│   │   └── __init__.py
│   ├── mixins.py
│   ├── models.py
│   ├── models_base.py
│   ├── plugin_base.py
│   ├── render_template.py
│   ├── segmentation/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── cms_plugins.py
│   │   ├── cms_toolbars.py
│   │   └── mixins.py
│   ├── sharable/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── fields.py
│   │   └── forms.py
│   ├── sphinx/
│   │   ├── __init__.py
│   │   ├── cms_apps.py
│   │   ├── cms_menus.py
│   │   ├── fragmentsbuilder.py
│   │   ├── link_plugin.py
│   │   ├── static/
│   │   │   └── cascade/
│   │   │       └── sphinx/
│   │   │           ├── css/
│   │   │           │   ├── bootstrap-sphinx.css
│   │   │           │   └── documentation.css
│   │   │           └── js/
│   │   │               └── link_plugin.js
│   │   └── theme/
│   │       └── bootstrap-fragments/
│   │           ├── globaltoc.html
│   │           ├── layout.html
│   │           └── theme.conf
│   ├── static/
│   │   └── cascade/
│   │       ├── LICENSE.md
│   │       ├── css/
│   │       │   └── admin/
│   │       │       ├── bootstrap4-buttons.css
│   │       │       ├── borderchoice.css
│   │       │       ├── cascadepage.css
│   │       │       ├── clipboard.css
│   │       │       ├── colorpicker.css
│   │       │       ├── editplugin.css
│   │       │       ├── iconfont.css
│   │       │       ├── iconplugin.css
│   │       │       ├── leafletplugin.css
│   │       │       ├── linkplugin.css
│   │       │       └── partialfields.css
│   │       └── js/
│   │           ├── admin/
│   │           │   ├── buttonmixin.js
│   │           │   ├── buttonplugin.js
│   │           │   ├── cascadepage.js
│   │           │   ├── clipboard.js
│   │           │   ├── colorpicker.js
│   │           │   ├── framediconplugin.js
│   │           │   ├── iconplugin.js
│   │           │   ├── iconpluginmixin.js
│   │           │   ├── imageplugin.js
│   │           │   ├── jumbotronplugin.js
│   │           │   ├── leafletplugin.js
│   │           │   ├── linkplugin.js
│   │           │   ├── pictureplugin.js
│   │           │   ├── segmentation.js
│   │           │   ├── segmentplugin.js
│   │           │   ├── sharableglossary.js
│   │           │   ├── sharedsettingsfield.js
│   │           │   ├── textimageplugin.js
│   │           │   └── textlinkplugin.js
│   │           ├── picturefill.js
│   │           ├── ring.js
│   │           └── underscore.js
│   ├── strides.py
│   ├── templates/
│   │   └── cascade/
│   │       ├── admin/
│   │       │   ├── change_form.html
│   │       │   ├── ckeditor.wysiwyg.txt
│   │       │   ├── clipboard_close_frame.html
│   │       │   ├── clipboard_paste_plugins.html
│   │       │   ├── clipboard_reload_page.html
│   │       │   ├── leaflet_plugin_change_form.html
│   │       │   ├── legacy_widgets/
│   │       │   │   ├── button_sizes.html
│   │       │   │   ├── button_types.html
│   │       │   │   ├── container_breakpoints.html
│   │       │   │   └── panel_types.html
│   │       │   ├── segmentation_list.html
│   │       │   ├── sharedglossary_change_form.html
│   │       │   └── widgets/
│   │       │       ├── borderchoice.html
│   │       │       ├── button_sizes.html
│   │       │       ├── button_types.html
│   │       │       ├── colorpicker.html
│   │       │       ├── container_breakpoints.html
│   │       │       ├── inherit_color.html
│   │       │       └── panel_types.html
│   │       ├── bootstrap4/
│   │       │   ├── accordion.html
│   │       │   ├── angular-ui/
│   │       │   │   ├── accordion.html
│   │       │   │   ├── carousel.html
│   │       │   │   └── tabset.html
│   │       │   ├── button.html
│   │       │   ├── card.html
│   │       │   ├── carousel-slide.html
│   │       │   ├── carousel.html
│   │       │   ├── framedicon.html
│   │       │   ├── image.html
│   │       │   ├── jumbotron.html
│   │       │   ├── linked-image.html
│   │       │   ├── linked-picture.html
│   │       │   ├── picture.html
│   │       │   ├── secmenu-list-group.html
│   │       │   ├── secmenu-list-item.html
│   │       │   ├── secmenu-unstyled-list-item.html
│   │       │   ├── secmenu-unstyled-list.html
│   │       │   ├── tabset.html
│   │       │   └── youtube.html
│   │       ├── generic/
│   │       │   ├── does_not_exist.html
│   │       │   ├── heading.html
│   │       │   ├── naked.html
│   │       │   ├── single.html
│   │       │   └── wrapper.html
│   │       ├── link/
│   │       │   ├── .editorconfig
│   │       │   ├── link-base-nostyle.html
│   │       │   ├── link-base.html
│   │       │   ├── text-link-linebreak.html
│   │       │   └── text-link.html
│   │       └── plugins/
│   │           ├── googlemap.html
│   │           ├── leaflet.html
│   │           ├── link.html
│   │           ├── simpleicon.html
│   │           ├── texticon.html
│   │           └── textimage.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── cascade_tags.py
│   ├── utils.py
│   └── widgets.py
├── docs/
│   ├── Makefile
│   ├── make.bat
│   └── source/
│       ├── _static/
│       │   └── shop_link_plugin.py
│       ├── bootstrap3/
│       │   ├── gallery.rst
│       │   ├── grid.rst
│       │   ├── image-picture.rst
│       │   ├── index.rst
│       │   ├── navbar.rst
│       │   └── other-components.rst
│       ├── bootstrap4/
│       │   ├── grid.rst
│       │   ├── index.rst
│       │   └── utilities.rst
│       ├── changelog.rst
│       ├── client-side.rst
│       ├── clipboard.rst
│       ├── conf.py
│       ├── customize-styles.rst
│       ├── customized-plugins.rst
│       ├── embeds.rst
│       ├── generic-plugins.rst
│       ├── hide-plugins.rst
│       ├── icon-fonts.rst
│       ├── impatient.rst
│       ├── index.rst
│       ├── installation.rst
│       ├── introduction.rst
│       ├── leaflet.rst
│       ├── link-plugin.rst
│       ├── release-notes-1.rst
│       ├── render-template.rst
│       ├── section.rst
│       ├── segmentation.rst
│       ├── sharable-fields.rst
│       ├── sphinx.rst
│       └── strides.rst
├── examples/
│   └── bs4demo/
│       ├── .coveragerc
│       ├── bs4demo/
│       │   ├── __init__.py
│       │   ├── cms_plugins.py
│       │   ├── context_processors.py
│       │   ├── models.py
│       │   ├── settings.py
│       │   ├── static/
│       │   │   └── bs4demo/
│       │   │       ├── cascades/
│       │   │       │   └── strides.json
│       │   │       └── css/
│       │   │           ├── _footer.scss
│       │   │           ├── _variables.scss
│       │   │           ├── badge.scss
│       │   │           └── main.scss
│       │   ├── templates/
│       │   │   └── bs4demo/
│       │   │       ├── badge.html
│       │   │       ├── base.html
│       │   │       ├── main.html
│       │   │       ├── strides.html
│       │   │       └── wrapped.html
│       │   ├── urls.py
│       │   └── utils.py
│       ├── manage.py
│       └── package.json
├── pytest.ini
├── requirements/
│   └── base.txt
├── setup.py
└── tests/
    ├── __init__.py
    ├── bootstrap4/
    │   ├── __init__.py
    │   ├── conftest.py
    │   ├── test_accordion.py
    │   ├── test_container.py
    │   └── test_grid.py
    ├── conftest.py
    ├── requirements.txt
    ├── settings.py
    ├── static/
    │   ├── strides/
    │   │   ├── bootstrap-button.json
    │   │   ├── bootstrap-column.json
    │   │   ├── bootstrap-container.json
    │   │   ├── bootstrap-jumbotron.json
    │   │   ├── bootstrap-row.json
    │   │   ├── carousel-plugin.json
    │   │   ├── framed-icon.json
    │   │   ├── simple-wrapper.json
    │   │   └── text-plugin.json
    │   └── strides.json
    ├── templates/
    │   └── testing.html
    ├── test_base.py
    ├── test_customplugin.py
    ├── test_http.py
    ├── test_iconfont.py
    ├── test_missingmigrations.py
    ├── test_segmentation.py
    ├── test_strides.py
    ├── urls.py
    └── utils.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
branch = True

[report]
show_missing = True
omit =
    .tox/*


================================================
FILE: .editorconfig
================================================
root = true

[*]
end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace = false

# Set default charset
[*.{js,py,rst,json,yml,html,txt,css,scss}]
charset = utf-8

[*.{js,py,rst,yml,html,scss}]
insert_final_newline = true
trim_trailing_whitespace = true

[*.py]
max_line_length =   119
indent_style = space
indent_size = 4

[*.{html,js,rst,css,scss}]
indent_style = tab
indent_size = 4

[*.rst]
max_line_length = 100

[*.{json,yml,css}]
indent_style = space
indent_size = 2


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish djangocms-cascade

on:
  push:
    tags:
      - '*'

jobs:
  publish:
    name: "Publish release"
    runs-on: "ubuntu-latest"

    environment:
       name: deploy

    strategy:
      matrix:
        python-version: ["3.9"]

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v3
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        python -m pip install twine
        python -m pip install build
    - name: Build 🐍 Python 📦 Package
      run: |
        python -m build --sdist --wheel --outdir dist/
        twine check --strict dist/*
    - name: Publish 🐍 Python 📦 Package to PyPI
      if: startsWith(github.ref, 'refs/tags')
      uses: pypa/gh-action-pypi-publish@release/v1
      with:
        user: __token__
        password: ${{ secrets.PYPI_API_TOKEN_CMSPLUGIN_CASCADE }}


================================================
FILE: .github/workflows/tests.yml
================================================
name: Test djangocms-cascade

on:
  push:
    branches: [ "master", "releases/*" ]
  pull_request:
    branches: [ master ]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.8", "3.9", "3.10"]
        django-version: ["3.1", "3.2"]

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    - name: Upgrade PIP
      run: python -m pip install --upgrade pip
    - name: Install Django-3.1
      if: matrix.django-version == '3.1'
      run: python -m pip install "Django<3.2"
    - name: Install Django-3.2
      if: matrix.django-version == '3.2'
      run: python -m pip install "Django<4.0"
    - name: Install Django-4.0
      if: matrix.django-version == '4.0'
      run: |
        python -m pip install "Django<4.1"
        python -m pip install "django-admin-sortable>=2.0"
    - name: Install dependencies
      run: |
        pip install -r requirements/base.txt
        pip install -r tests/requirements.txt
    - name: Lint with flake8
      run: |
        pip install flake8
        # stop the build if there are Python syntax errors or undefined names
        flake8 . --count --select=E9,F63,F7,F82 --ignore=F821 --show-source --statistics
        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
    - name: Test with pytest
      run: |
        python -m pytest tests


================================================
FILE: .gitignore
================================================
.idea
*.log
*.pot
*.pyc
.project
.pydevproject
.settings
.coverage
.DS_Store
.sass-cache
.cache
.pytest_cache
*.db
*.egg-info
.eggs
.tox
build
env*
dist
htmlcov
bower_components/
node_modules/
workdir/
filer_public/
filer_public_thumbnails/
examples/bs3demo/private_settings.py
.venv
package-lock.json
poetry.lock


================================================
FILE: BOOSTRAP-4.md
================================================
# Migrating towards Bootstrap-4

Currently I'am refactoring **djangocms-cascade**, adding support for Bootstrap-4. This is not as
easy as one might expect, because of the way Bootstrap-4 handles the width of columns. Remember,
in Bootstrap-3 the width of each column was predictable. With the advent of auto-columns in
Bootstrap-4, this isn't possible anymore.


## Why does this matter?

One of **djangocms-cascade**'s great features is, that it always keeps track of the column
width for each available breakpoint. This is important, so that responsive images can be
thumbnailed to fit exactly into a given column and hence save bandwidth. Therefore in Bootstrap-3,
Cascade rendered images with up to 8 thumbnails in different sizes. From these the browser then is
able to choose that one, which best fits into the given viewport.

This in modern HTML is handled by the `<img ...>` element itself, using the attributes `sizes`
together with `srcset`. The `<picture>` element, uses the children elements `<source ...>`.


## In Bootstrap-4, this approach doesn't work anymore 

In Bootstrap-4, containers use the flexbox model rather than floating divs. This allows the use
of columns with equally distributed width, in addition to the fixed sized columns we already know.
Therefore the widths of the columns can only be calculated, if we know how their siblings are made
up. Therefore the existing approach to determine the widths of columns used by **djangocms-cascade**
doesn't work anymore.

Therefore uses another approach.

Instead of keeping track on the widths of containers, rows and columns, version 0.17 of
**djangocms-cascade** calculates this only for images.


In the meantime I tried to come up with alternative ideas for my problem with computing image
widths.
As you know, in Cascade-BS3, for each image 4 different (8 for retina) widths are computed and
delivered with their <img srcset="...">. This was relatively easy, since in BS-3 we had to deal
with pixels and the widths of columns was determinable. As much as I bump my head against the table,
I can't find a generic solution for BS-4. Consider columns such as `<div class="col-auto">some content</div>`,
how do you determine the maximum width in order to thumbnail the image and compute the srcset-s?
But even without auto-width columns, it gets really difficult to get all edge cases.


================================================
FILE: LICENSE-MIT
================================================
The MIT License (MIT)

Copyright (c) 2013 Jacob Rief

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include LICENSE-MIT
include README.md
include setup.py
recursive-include cmsplugin_cascade/locale *
recursive-include cmsplugin_cascade/static *
recursive-include cmsplugin_cascade/templates *
recursive-include cmsplugin_cascade *.py
recursive-include cmsplugin_cascade/sphinx/static *
recursive-include cmsplugin_cascade/sphinx/theme *
recursive-exclude tests *
recursive-exclude * *.pyc


================================================
FILE: README.md
================================================
# djangocms-cascade

[![Build Status](https://github.com/jrief/djangocms-cascade/actions/workflows/tests.yml/badge.svg)](https://github.com/jrief/djangocms-cascade/actions)
[![PyPI version](https://img.shields.io/pypi/v/djangocms-cascade.svg)](https://pypi.python.org/pypi/djangocms-cascade)
[![Django versions](https://img.shields.io/pypi/djversions/djangocms-cascade)](https://pypi.python.org/pypi/djangocms-cascade)
[![Python versions](https://img.shields.io/pypi/pyversions/djangocms-cascade.svg)](https://pypi.python.org/pypi/djangocms-cascade)
[![Software license](https://img.shields.io/pypi/l/djangocms-cascade.svg)](https://github.com/jrief/djangocms-cascade/blob/master/LICENSE)

The Swiss army knife for working with Django-CMS plugins.

## Why Use DjangoCMS-Cascade?

**DjangoCMS-Cascade** is a collection of plugins for Django-CMS
[placeholders](http://docs.django-cms.org/en/latest/introduction/templates_placeholders.html#templates-placeholders).
Instead of creating one database model for each CMS plugin, Cascade shares one database model for
all of them. The payload then is stored inside a JSON field instead of declaring each attribute
explicitly. This furthermore prevents us to handle all kind of nasty database migration problems.


## Features

### Perfect for nested grid systems

Since **Cascade** keeps track on the widths of all columns, ``<img>`` and ``<picture>`` elements can
be rendered in a responsive way, so that the browser only loads the image required for the visible
viewport.


### Extend plugins with additional features

Using a JSON field to store the payload gives us much more flexibility. We can for instance enrich
our plugins with additional attributes, configured during runtime. This can be used to optionally
share attributes across different plugins (referenced by an alias name), add CSS classes and styles,
or offer alternative rendering templates.


### Set links onto your own views

Another nice aspect of this approach is, that we can override the functionality used to set links
onto pages which are not part of the CMS. This is specially useful, since we do not want to
re-implement this functionality for all plugins, which require links, ie. images, pictures,
buttons and text-links.


### Copy content and paste it somewhere else

Since the payload of plugins is already serialized, we can even copy them from one site to another
site supporting **djangocms-cascade**.


## Documentation

Find detailed documentation on [ReadTheDocs](http://djangocms-cascade.readthedocs.io/en/latest/).

Please see the [Release Notes](http://djangocms-cascade.readthedocs.io/en/latest/changelog.html)
before upgrading from an older version.


## Architecture

### It's pluggable

**DjangoCMS-Cascade** is very modular, keeping its CMS modules in functional groups. These groups
have to be activated independently in the project's `settings.py`. It also is possible to activate
only certain plugins out of a group. Currently Bootstrap-4 is implemented, but this app could
easily be extended for other CSS frameworks.

### Configurable individually

Each **Cascade** plugin can be styled individually. The site-administrator can specify which CSS
styles and CSS classes can be added to each plugin. Then the page-editor can pick one of the
allowed styles to adopt his elements accordingly.


### Reuse your data

Each **Cascade** plugin can be configured by the site-administrator to share some or all of its
data fields. This for instance is handy, to keep references onto external URLs in a central place.
Or is can be used to resize all images sharing a cetrain property in one go.


### Segment the DOM

It is even possible to group plugins into seperate evaluation contexts. This for instance is used
to render different Plugins, depending on whether a user is authenticated or anonymous.


### Responsive Images

In modern web development, images must adopt to the column width in which they are rendered.
Therefore the ``<img ...>`` tag, in addition to the well known ``src`` attribute, also accepts
additional ``srcset``'s, one for each media query. Here **djangocms-cascade** calculates the
required widths for each image, depending on the current column layout considering all media
breakpoints.

This is also implemented for the ``<picture>`` element with all of it's children, normally
``<source srcset="...">``.

It also supports resolutions of more than one physical pixel per logical pixel as found in Retina
displays.


### Other Features

* Use the scaffolding technique from the preferred CSS framework to subdivide a placeholder into a
  [grid system](http://getbootstrap.com/css/#grid).
* Make full usage of responsive techniques, by allowing
  [stacked to horizontal](http://getbootstrap.com/css/#grid-example-basic) classes per element.
* Use styled [buttons](http://getbootstrap.com/css/#buttons) to add links.
* Wrap special content into a [Jumbotron](http://getbootstrap.com/components/#jumbotron) or a
  [Carousel](http://getbootstrap.com/javascript/#carousel).
* Add ``<img>`` and ``<picture>`` elements in a responsive way, so that more than one image URL
  points onto the resized sources, one for each viewport using the ``srcset`` tags or the
  ``<source>`` elements.
* Use segmentation to conditionally render parts of the DOM.
* Temporarily hide a plugin to show up in the DOM.
* Upload an self composed font from [Fontello](http://fontello.com/) and use it's icon in plain text
  or as framed eye catchers.
* It is very easy to integrate additional elements from the preferred CSS framework. For instance,
  implementing the Bootstrap Carousel, requires only 50 lines of Python code and two simple Django
  templates.
* Since all the data is stored in JSON, no database migration is required if a field is added,
  modified or removed from the plugin.
* Currently **Bootstrap-4** is supported, but other CSS frameworks can be easily added in a
  pluggable manner.
* It follows the "batteries included" philosophy, but still remains very modular.

In addition to easily implement any kind of plugin, **DjangoCMS-Cascade** makes it possible to add
reusable helpers. Such a helper enriches a plugin with an additional, configurable functionality:

* By making some of the plugin fields sharable, one can reuse these values for other plugins of the
  same kind. This for instance is handy for the image and picture plugin, so that images always are
  resized to predefined values.
* By allowing extra fields, one can add an optional ``id`` tag, CSS classes and inline styles. This
  is configurable on a plugin and site base.
* It is possible to customize the rendering templates shipped with the plugins.
* Since all data is JSON, you can dump the content of one placeholder and insert it into another one,
  even on a foreign site. This for instance is useful to transfer pages from the staging site to production.


### Help appreciated

If someone wants to start a subproject for a CSS framework, other than Bootstrap-4/5.

If you are a native English speaker, please check the documentation for spelling mistakes and
grammar, since English is not my mother tongue.


[![Twitter](https://img.shields.io/twitter/follow/jacobrief?style=social)](https://twitter.com/jacobrief)


================================================
FILE: cmsplugin_cascade/__init__.py
================================================
"""
See PEP 386 (https://www.python.org/dev/peps/pep-0386/)

Release logic:
 1. Remove ".devX" from __version__ (below)
 2. Remove ".devX" latest version in docs/source/changelog.rst
 3. git add cmsplugin_cascade/__init__.py docs/source/changelog.rst
 4. git commit -m 'Bump to <version>'
 5. git tag <version>
 6. git push
 7. assure that all tests pass on https://github.com/jrief/django-formset/actions/workflows/tests.yml
 8. git push --tags
 9. assure that a new version is published on https://github.com/jrief/django-formset/actions/workflows/publish.yml
10. bump the version, append ".dev0" to __version__
11. Add a new heading to docs/source/changelog.rst, named "<next-version>.dev0"
12. git add cmsplugin_cascade/__init__.py docs/source/changelog.rst
13. git commit -m 'Start with <version>'
14. git push
"""
__version__ = "2.3.14"

default_app_config = 'cmsplugin_cascade.apps.CascadeConfig'


================================================
FILE: cmsplugin_cascade/admin.py
================================================
from urllib.parse import urlparse
import requests

from django.contrib import admin
from django.contrib.sites.shortcuts import get_current_site
from django.forms import Media, widgets
from django.db.models import Q
from django.http import JsonResponse, HttpResponseForbidden, HttpResponseNotFound
from django.urls import re_path
from django.utils.translation import get_language_from_request, get_language_from_path

from cms.models.pagemodel import Page
from cms.extensions import PageExtensionAdmin
from cms.utils.page import get_pages_from_path
from cmsplugin_cascade.models import CascadePage, IconFont
from cmsplugin_cascade.link.forms import format_page_link


@admin.register(CascadePage)
class CascadePageAdmin(PageExtensionAdmin):
    add_form_template = change_form_template = 'cascade/admin/change_form.html'
    fields = ['icon_font', 'menu_symbol']

    @property
    def media(self):
        media = super().media
        media += Media(css={'all': ['cascade/css/admin/cascadepage.css']},
                       js=['admin/js/jquery.init.js', 'cascade/js/admin/cascadepage.js'])
        return media

    def get_form(self, request, obj=None, **kwargs):
        options = dict(kwargs, widgets={'menu_symbol': widgets.HiddenInput})
        ModelForm = super().get_form(request, obj, **options)
        return ModelForm

    def get_urls(self):
        urls = [
            re_path(r'^get_page_sections/$', lambda _: JsonResponse({'element_ids': []}),
                name='get_page_sections'),  # just to reverse
            re_path(r'^get_page_sections/(?P<page_pk>\d+)$',
                self.admin_site.admin_view(self.get_page_sections)),
            re_path(r'^published_pages/$', self.get_published_pagelist, name='get_published_pagelist'),
            re_path(r'^fetch_fonticons/(?P<iconfont_id>[0-9]+)$', self.fetch_fonticons),
            re_path(r'^fetch_fonticons/$', self.fetch_fonticons, name='fetch_fonticons'),
            re_path(r'^validate_exturl/$', self.validate_exturl, name='validate_exturl'),
        ]
        urls.extend(super().get_urls())
        return urls

    def get_page_sections(self, request, page_pk=None):
        choices = []
        language = get_language_from_request(request, check_path=True)
        try:
            cascade_page = self.model.objects.get(extended_object_id=page_pk)
            extended_glossary = cascade_page.glossary
            for key, val in extended_glossary['element_ids'][language].items():
                if val:
                    choices.append((key, val))
        except (self.model.DoesNotExist, KeyError):
            pass
        return JsonResponse({'element_ids': choices})

    def get_published_pagelist(self, request, *args, **kwargs):
        """
        This view is used by the SearchLinkField as the user types to feed the autocomplete drop-down.
        """
        if request.headers.get('x-requested-with') != 'XMLHttpRequest':
            return HttpResponseForbidden()
        data = {'results': []}
        language = get_language_from_request(request, check_path=True)
        query_term = request.GET.get('term').strip()
        if not query_term:
            return JsonResponse(data)

        # first, try to resolve by URL if it points to a local resource
        parse_result = urlparse(query_term)
        if parse_result.netloc.split(':')[0] == request.META['HTTP_HOST'].split(':')[0]:
            site = get_current_site(request)
            path = parse_result.path.strip('/')
            if get_language_from_path(parse_result.path):
                path = '/'.join(path.split('/')[1:])
            pages = get_pages_from_path(site, path)
            for page in pages:
                data['results'].append(self.get_result_set(language, page))
            if len(data['results']) > 0:
                return JsonResponse(data)

        # otherwise resolve by search term
        matching_published_pages = Page.objects.published().public().filter(
            Q(title_set__title__icontains=query_term, title_set__language=language)
            | Q(title_set__path__icontains=query_term, title_set__language=language)
            | Q(title_set__menu_title__icontains=query_term, title_set__language=language)
            | Q(title_set__page_title__icontains=query_term, title_set__language=language)
        ).distinct().order_by('title_set__title').iterator()

        for page in matching_published_pages:
            data['results'].append(self.get_result_set(language, page))
            if len(data['results']) > 15:
                break
        return JsonResponse(data)

    def get_result_set(self, language, page):
        title = page.get_title(language=language)
        path = page.get_absolute_url(language=language)
        return {
            'id': page.pk,
            'text': format_page_link(title, path),
        }

    def fetch_fonticons(self, request, iconfont_id=None):
        try:
            icon_font = IconFont.objects.get(id=iconfont_id)
        except IconFont.DoesNotExist:
            return HttpResponseNotFound("IconFont with id={} does not exist".format(iconfont_id))
        else:
            data = dict(icon_font.config_data)
            data.pop('glyphs', None)
            data['families'] = icon_font.get_icon_families()
            return JsonResponse(data)

    def validate_exturl(self, request):
        """
        Perform a GET request onto the given external URL and return its status.
        """
        exturl = request.GET.get('exturl')
        if not exturl:
            return JsonResponse({})
        request_headers = {'User-Agent': 'Django-CMS-Cascade'}
        try:
            response = requests.get(exturl, allow_redirects=True, headers=request_headers, timeout=5.0)
        except Exception:
            return JsonResponse({'status_code': 500})
        else:
            return JsonResponse({'status_code': response.status_code})

    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        extra_context = dict(extra_context or {}, icon_fonts=IconFont.objects.all())
        return super().changeform_view(
             request, object_id=object_id, form_url=form_url, extra_context=extra_context)


================================================
FILE: cmsplugin_cascade/app_settings.py
================================================

class AppSettings:

    def _setting(self, name, default=None):
        from django.conf import settings
        return getattr(settings, name, default)

    @property
    def CASCADE_PLUGINS(self):
        return self._setting('CMSPLUGIN_CASCADE_PLUGINS', (
            'cmsplugin_cascade.generic',
            'cmsplugin_cascade.icon',
            'cmsplugin_cascade.link',
        ))

    @property
    def CMSPLUGIN_CASCADE(self):
        import os
        from collections import OrderedDict
        from importlib import import_module
        from django.forms.fields import NumberInput
        from django.core.exceptions import ImproperlyConfigured
        from django.utils.translation import gettext_lazy
        from cmsplugin_cascade.fields import (ColorField, SelectTextAlignField, SelectOverflowField, SizeField,
                                              BorderChoiceField)

        if hasattr(self, '_config_CMSPLUGIN_CASCADE'):
            return self._config_CMSPLUGIN_CASCADE

        INSTALLED_APPS = self._setting('INSTALLED_APPS')
        config = self._setting('CMSPLUGIN_CASCADE', {})
        config.setdefault('alien_plugins', ['TextPlugin'])
        config.setdefault('color_picker_with_alpha', False)
        config.setdefault('plugin_prefix', None)

        config.setdefault('plugins_with_extra_fields', {})
        if 'cmsplugin_cascade.extra_fields' in INSTALLED_APPS:
            from cmsplugin_cascade.extra_fields.config import PluginExtraFieldsConfig

            plugins_with_extra_fields = config['plugins_with_extra_fields']
            plugins_with_extra_fields.setdefault('SimpleWrapperPlugin', PluginExtraFieldsConfig())
            for plugin, plugin_config in plugins_with_extra_fields.items():
                if not isinstance(plugin_config, PluginExtraFieldsConfig):
                    msg = "CMSPLUGIN_CASCADE['plugins_with_extra_fields']['{}'] must instantiate a class of type PluginExtraFieldsConfig"
                    raise ImproperlyConfigured(msg.format(plugin))

        config.setdefault('plugins_with_extra_mixins', {})

        config.setdefault('plugins_with_sharables', {})
        if 'cmsplugin_cascade.sharable' in INSTALLED_APPS:
            config['plugins_with_sharables'].setdefault(
                'FramedIconPlugin',
                ['font_size', 'color', 'background_color', 'text_align', 'border', 'border_radius'])

        config['exclude_hiding_plugin'] = list(config.get('exclude_hiding_plugin', []))
        config['exclude_hiding_plugin'].append('SegmentPlugin')

        config.setdefault('link_plugin_classes', (
            'cmsplugin_cascade.link.plugin_base.DefaultLinkPluginBase',
            'cmsplugin_cascade.link.forms.LinkForm'))

        config['plugins_with_bookmark'] = list(config.get('plugins_with_bookmark', []))
        config['plugins_with_bookmark'].extend(['SimpleWrapperPlugin', 'HeadingPlugin'])
        config.setdefault('bookmark_prefix', '')

        config.setdefault('extra_inline_styles', OrderedDict())
        extra_inline_styles = config['extra_inline_styles']
        extra_inline_styles.setdefault(
            'Margins',
            (['margin-top', 'margin-right', 'margin-bottom', 'margin-left'], SizeField)
        )
        extra_inline_styles.setdefault(
            'Paddings',
            (['padding-top', 'padding-right', 'padding-bottom', 'padding-left'], SizeField)
        )
        extra_inline_styles.setdefault(
            'Widths',
            (['min-width', 'width', 'max-width'], SizeField)
        )
        extra_inline_styles.setdefault(
            'Heights',
            (['min-height', 'height', 'max-height'], SizeField)
        )
        extra_inline_styles.setdefault(
            'Text Alignement',
            (['text-align'], SelectTextAlignField)
        )
        extra_inline_styles.setdefault(
            'Font Size',
            (['font-size'], SizeField)
        )
        extra_inline_styles.setdefault(
            'Line Height',
            (['line-height'], NumberInput)
        )
        extra_inline_styles.setdefault(
            'Colors',
            (['color', 'background-color'], ColorField)
        )
        extra_inline_styles.setdefault(
            'Border',
            (['border', 'border-top', 'border-right', 'border-bottom', 'border-left'], BorderChoiceField)
        )
        extra_inline_styles.setdefault(
            'Border Radius',
            (['border-radius'], SizeField)
        )
        extra_inline_styles.setdefault(
            'Overflow',
            (['overflow', 'overflow-x', 'overflow-y'], SelectOverflowField)
        )

        if 'cmsplugin_cascade.segmentation' in INSTALLED_APPS:
            config.setdefault('segmentation_mixins', [
                ('cmsplugin_cascade.segmentation.mixins.EmulateUserModelMixin',
                 'cmsplugin_cascade.segmentation.mixins.EmulateUserAdminMixin')])

        config.setdefault(
            'icon_font_root',
            os.path.abspath(os.path.join(self._setting('MEDIA_ROOT'), 'icon_fonts')))

        config.setdefault('plugins_with_extra_render_templates', {})
        config['plugins_with_extra_render_templates'].setdefault(
            'TextLinkPlugin',
            [('cascade/link/text-link.html', gettext_lazy("default")),
             ('cascade/link/text-link-linebreak.html', gettext_lazy("with line break")),])
        config['plugins_with_extra_render_templates'].setdefault(
            'LeafletPlugin',
            [('cascade/plugins/leaflet.html', gettext_lazy("default")),
             ('cascade/plugins/googlemap.html', gettext_lazy("Google Map")),])

        config.setdefault('allow_plugin_hiding', False)

        config.setdefault('cache_strides', True)

        config.setdefault('register_page_editor', True)

        for module_name in self.CASCADE_PLUGINS:
            try:
                settings_module = import_module('{}.settings'.format(module_name))
                getattr(settings_module, 'set_defaults')(config)
            except (ImportError, AttributeError):
                continue

        self._config_CMSPLUGIN_CASCADE = config
        return config

    @property
    def CSS_PREFIXES(self):
        return {
            'image_set': ['-webkit-image-set', '-moz-image-set', '-o-image-set', '-ms-image-set', 'image-set'],
        }

    @property
    def RESPONSIVE_IMAGE_MAX_STEPS(self):
        """
        Responsive images are offered in a set of various widths. This number specifies the maximum number of
        generated thumbnails for a specific ``srcset`` of an image.
        """
        return 12

    @property
    def RESPONSIVE_IMAGE_STEP_SIZE(self):
        """
        Responsive images are offered in a set of various widths. This number specifies the minimum step width
        in pixels between the generated thumbnails for a specific ``srcset`` of an image. If the resulting number
        of steps would exceed ``RESPONSIVE_IMAGE_MAX_STEPS``, then a higher step width is used.
        """
        return 50

import sys  # noqa
app_settings = AppSettings()
app_settings.__name__ = __name__
sys.modules[__name__] = app_settings


================================================
FILE: cmsplugin_cascade/apps.py
================================================
from django.apps import AppConfig, apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.models.signals import pre_migrate, post_migrate
from django.db.utils import DatabaseError
from django.urls import reverse
from django.utils.translation import gettext_lazy as _


class CascadeConfig(AppConfig):
    name = 'cmsplugin_cascade'
    verbose_name = _("django CMS Cascade")
    default_permissions = ('add', 'change', 'delete')

    def ready(self):
        if 'django_select2' not in settings.INSTALLED_APPS:
            raise ImproperlyConfigured('django_select2 not configured')

        stylesSet = str(settings.CKEDITOR_SETTINGS.get('stylesSet'))
        if stylesSet != 'default:{}'.format(reverse('admin:cascade_texteditor_config')):
            msg = "settings.CKEDITOR_SETTINGS['stylesSet'] should be `format_lazy('default:{}', reverse_lazy('admin:cascade_texteditor_config'))`"
            raise ImproperlyConfigured(msg)

        pre_migrate.connect(self.__class__.pre_migrate, sender=self)
        post_migrate.connect(self.__class__.post_migrate, sender=self)

    @classmethod
    def pre_migrate(cls, sender=None, **kwargs):
        """
        Iterate over contenttypes and remove those not in proxy models
        """
        ContentType = apps.get_model('contenttypes', 'ContentType')
        try:
            queryset = ContentType.objects.filter(app_label=sender.label)
            for ctype in queryset.exclude(model__in=sender.get_proxy_models().keys()):
                model = ctype.model_class()
                if model is None:
                    sender.revoke_permissions(ctype)
                    ContentType.objects.get(app_label=sender.label, model=ctype).delete()
        except DatabaseError:
            return

    @classmethod
    def post_migrate(cls, sender=None, **kwargs):
        """
        Iterate over fake_proxy_models and add contenttypes and permissions for missing proxy
        models, if this has not been done by Django yet
        """
        ContentType = apps.get_model('contenttypes', 'ContentType')

        for model_name, proxy_model in sender.get_proxy_models().items():
            ctype, created = ContentType.objects.get_or_create(app_label=sender.label, model=model_name)
            if created:
                sender.grant_permissions(proxy_model)

    def get_proxy_models(self):
        from cmsplugin_cascade.plugin_base import fake_proxy_models

        proxy_models = {}
        for model_name in fake_proxy_models.keys():
            proxy_model = self.get_model(model_name)
            model_name = proxy_model._meta.model_name  # the model_name in lowercase normally
            proxy_models[model_name] = proxy_model
        return proxy_models

    def grant_permissions(self, proxy_model):
        """
        Create the default permissions for the just added proxy model
        """
        ContentType = apps.get_model('contenttypes', 'ContentType')
        try:
            Permission = apps.get_model('auth', 'Permission')
        except LookupError:
            return

        # searched_perms will hold the permissions we're looking for as (content_type, (codename, name))
        searched_perms = []
        ctype = ContentType.objects.get_for_model(proxy_model)
        for perm in self.default_permissions:
            searched_perms.append((
                '{0}_{1}'.format(perm, proxy_model._meta.model_name),
                "Can {0} {1}".format(perm, proxy_model._meta.verbose_name_raw)
            ))

        all_perms = set(Permission.objects.filter(
            content_type=ctype,
        ).values_list(
            'content_type', 'codename'
        ))
        permissions = [
            Permission(codename=codename, name=name, content_type=ctype)
            for codename, name in searched_perms if (ctype.pk, codename) not in all_perms
        ]
        Permission.objects.bulk_create(permissions)

    def revoke_permissions(self, ctype):
        """
        Remove all permissions for the content type to be removed
        """
        ContentType = apps.get_model('contenttypes', 'ContentType')
        try:
            Permission = apps.get_model('auth', 'Permission')
        except LookupError:
            return

        codenames = ['{0}_{1}'.format(perm, ctype) for perm in self.default_permissions]
        cascade_element = apps.get_model(self.label, 'cascadeelement')
        element_ctype = ContentType.objects.get_for_model(cascade_element)
        Permission.objects.filter(content_type=element_ctype, codename__in=codenames).delete()


================================================
FILE: cmsplugin_cascade/bootstrap4/__init__.py
================================================


================================================
FILE: cmsplugin_cascade/bootstrap4/accordion.py
================================================
from django.forms import widgets, BooleanField, CharField
from django.forms.fields import IntegerField
from django.utils.translation import ngettext_lazy, gettext_lazy as _
from django.utils.safestring import mark_safe
from django.utils.text import Truncator
from django.utils.html import escape
from entangled.forms import EntangledModelFormMixin
from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.forms import ManageChildrenFormMixin
from cmsplugin_cascade.plugin_base import TransparentWrapper, TransparentContainer
from cmsplugin_cascade.widgets import NumberInputWidget
from .plugin_base import BootstrapPluginBase


class AccordionFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin):
    num_children = IntegerField(
        min_value=1,
        initial=1,
        widget=NumberInputWidget(attrs={'size': '3', 'style': 'width: 5em !important;'}),
        label=_("Groups"),
        help_text=_("Number of groups for this accordion."),
    )

    close_others = BooleanField(
         label=_("Close others"),
         initial=True,
         required=False,
         help_text=_("Open only one card at a time.")
    )

    first_is_open = BooleanField(
         label=_("First open"),
         initial=True,
         required=False,
         help_text=_("Start with the first card open.")
    )

    class Meta:
        untangled_fields = ['num_children']
        entangled_fields = {'glossary': ['close_others', 'first_is_open']}


class BootstrapAccordionPlugin(TransparentWrapper, BootstrapPluginBase):
    name = _("Accordion")
    default_css_class = 'accordion'
    require_parent = True
    parent_classes = ['BootstrapColumnPlugin']
    direct_child_classes = ['BootstrapAccordionGroupPlugin']
    allow_children = True
    form = AccordionFormMixin
    render_template = 'cascade/bootstrap4/{}accordion.html'

    @classmethod
    def get_identifier(cls, obj):
        num_cards = obj.get_num_children()
        content = ngettext_lazy('with {0} card', 'with {0} cards', num_cards).format(num_cards)
        return mark_safe(content)

    def render(self, context, instance, placeholder):
        context = self.super(BootstrapAccordionPlugin, self).render(context, instance, placeholder)
        context.update({
            'close_others': instance.glossary.get('close_others', True),
            'first_is_open': instance.glossary.get('first_is_open', True),
        })
        return context

    def save_model(self, request, obj, form, change):
        wanted_children = int(form.cleaned_data.get('num_children'))
        super().save_model(request, obj, form, change)
        self.extend_children(obj, wanted_children, BootstrapAccordionGroupPlugin)

plugin_pool.register_plugin(BootstrapAccordionPlugin)


class AccordionGroupFormMixin(EntangledModelFormMixin):
    heading = CharField(
        label=_("Heading"),
        widget=widgets.TextInput(attrs={'size': 80}),
    )

    body_padding = BooleanField(
         label=_("Body with padding"),
         initial=True,
         required=False,
         help_text=_("Add standard padding to card body."),
    )

    class Meta:
        entangled_fields = {'glossary': ['heading', 'body_padding']}

    def clean_heading(self):
        return escape(self.cleaned_data['heading'])


class BootstrapAccordionGroupPlugin(TransparentContainer, BootstrapPluginBase):
    name = _("Accordion Group")
    direct_parent_classes = parent_classes = ['BootstrapAccordionPlugin']
    render_template = 'cascade/generic/naked.html'
    require_parent = True
    form = AccordionGroupFormMixin
    alien_child_classes = True

    @classmethod
    def get_identifier(cls, instance):
        heading = instance.glossary.get('heading', '')
        return Truncator(heading).words(3, truncate=' ...')

    def render(self, context, instance, placeholder):
        context = self.super(BootstrapAccordionGroupPlugin, self).render(context, instance, placeholder)
        context.update({
            'heading': mark_safe(instance.glossary.get('heading', '')),
            'no_body_padding': not instance.glossary.get('body_padding', True),
        })
        return context

plugin_pool.register_plugin(BootstrapAccordionGroupPlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/buttons.py
================================================
from django.forms import widgets
from django.forms.fields import BooleanField, CharField, ChoiceField, MultipleChoiceField
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from entangled.forms import EntangledModelFormMixin
from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.icon.plugin_base import IconPluginMixin
from cmsplugin_cascade.icon.forms import IconFormMixin
from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin
from cmsplugin_cascade.link.plugin_base import LinkElementMixin


class ButtonTypeWidget(widgets.RadioSelect):
    """
    Render sample buttons in different colors in the button's backend editor.
    """
    template_name = 'cascade/admin/widgets/button_types.html'


class ButtonSizeWidget(widgets.RadioSelect):
    """
    Render sample buttons in different sizes in the button's backend editor.
    """
    template_name = 'cascade/admin/widgets/button_sizes.html'


class ButtonFormMixin(EntangledModelFormMixin):
    BUTTON_TYPES = [
        ('btn-primary', _("Primary")),
        ('btn-secondary', _("Secondary")),
        ('btn-success', _("Success")),
        ('btn-danger', _("Danger")),
        ('btn-warning', _("Warning")),
        ('btn-info', _("Info")),
        ('btn-light', _("Light")),
        ('btn-dark', _("Dark")),
        ('btn-link', _("Link")),
        ('btn-outline-primary', _("Primary")),
        ('btn-outline-secondary', _("Secondary")),
        ('btn-outline-success', _("Success")),
        ('btn-outline-danger', _("Danger")),
        ('btn-outline-warning', _("Warning")),
        ('btn-outline-info', _("Info")),
        ('btn-outline-light', _("Light")),
        ('btn-outline-dark', _("Dark")),
        ('btn-outline-link', _("Link")),
    ]

    BUTTON_SIZES = [
        ('btn-lg', _("Large button")),
        ('', _("Default button")),
        ('btn-sm', _("Small button")),
    ]

    link_content = CharField(
        required=False,
        label=_("Button Content"),
        widget=widgets.TextInput(attrs={'size': 50}),
    )

    button_type = ChoiceField(
        label=_("Button Type"),
        widget=ButtonTypeWidget(choices=BUTTON_TYPES),
        choices=BUTTON_TYPES,
        initial='btn-primary',
        help_text=_("Display Link using this Button Style")
    )

    button_size = ChoiceField(
        label=_("Button Size"),
        widget=ButtonSizeWidget(choices=BUTTON_SIZES),
        choices=BUTTON_SIZES,
        initial='',
        required=False,
        help_text=_("Display Link using this Button Size")
    )

    button_options = MultipleChoiceField(
        label=_("Button Options"),
        choices=[
            ('btn-block', _('Block level')),
            ('disabled', _('Disabled')),
        ],
        required=False,
        widget=widgets.CheckboxSelectMultiple,
    )

    stretched_link = BooleanField(
        label=_("Stretched link"),
        required=False,
        help_text=_("Stretched-link utility to make any anchor the size of it’s nearest position: " \
                    "relative parent, perfect for entirely clickable cards!")
    )

    icon_align = ChoiceField(
        label=_("Icon alignment"),
        choices=[
            ('icon-left', _("Icon placed left")),
            ('icon-right', _("Icon placed right")),
        ],
        widget=widgets.RadioSelect,
        initial='icon-right',
        help_text=_("Add an Icon before or after the button content."),
    )

    class Meta:
        entangled_fields = {'glossary': ['link_content', 'button_type', 'button_size', 'button_options', 'icon_align',
                                         'stretched_link']}


class BootstrapButtonMixin(IconPluginMixin):
    require_parent = True
    parent_classes = ['BootstrapColumnPlugin', 'SimpleWrapperPlugin']
    render_template = 'cascade/bootstrap4/button.html'
    allow_children = False
    default_css_class = 'btn'
    default_css_attributes = ['button_type', 'button_size', 'button_options', 'stretched_link']
    ring_plugin = 'ButtonMixin'

    class Media:
        css = {'all': ['cascade/css/admin/bootstrap4-buttons.css', 'cascade/css/admin/iconplugin.css']}
        js = ['admin/js/jquery.init.js', 'cascade/js/admin/buttonmixin.js']

    def render(self, context, instance, placeholder):
        context = super().render(context, instance, placeholder)
        if 'icon_font_class' in context:
            mini_template = '{0}<i class="{1} {2}" aria-hidden="true"></i>{3}'
            icon_align = instance.glossary.get('icon_align')
            if icon_align == 'icon-left':
                context['icon_left'] = format_html(mini_template, '', context['icon_font_class'], 'cascade-icon-left',
                                                   ' ')
            elif icon_align == 'icon-right':
                context['icon_right'] = format_html(mini_template, ' ', context['icon_font_class'],
                                                    'cascade-icon-right', '')
        return context


class BootstrapButtonFormMixin(LinkFormMixin, IconFormMixin, ButtonFormMixin):
    require_link = False
    require_icon = False


class BootstrapButtonPlugin(BootstrapButtonMixin, LinkPluginBase):
    module = 'Bootstrap'
    name = _("Button")
    model_mixins = (LinkElementMixin,)
    form = BootstrapButtonFormMixin
    ring_plugin = 'ButtonPlugin'
    DEFAULT_BUTTON_ATTRIBUTES = {'role': 'button'}

    class Media:
        js = ['admin/js/jquery.init.js', 'cascade/js/admin/buttonplugin.js']

    @classmethod
    def get_identifier(cls, instance):
        content = instance.glossary.get('link_content')
        if not content:
            try:
                button_types = dict(ButtonFormMixin.BUTTON_TYPES)
                content = str(button_types[instance.glossary['button_type']])
            except KeyError:
                content = _("Empty")
        return content

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = cls.super(BootstrapButtonPlugin, cls).get_css_classes(obj)
        if obj.glossary.get('stretched_link'):
            css_classes.append('stretched_link')
        return css_classes

    @classmethod
    def get_html_tag_attributes(cls, obj):
        attributes = cls.super(BootstrapButtonPlugin, cls).get_html_tag_attributes(obj)
        attributes.update(cls.DEFAULT_BUTTON_ATTRIBUTES)
        return attributes

    def render(self, context, instance, placeholder):
        context = self.super(BootstrapButtonPlugin, self).render(context, instance, placeholder)
        return context

plugin_pool.register_plugin(BootstrapButtonPlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/card.py
================================================
from django.utils.translation import gettext_lazy as _
from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.plugin_base import TransparentContainer, TransparentWrapper
from cmsplugin_cascade.bootstrap4.plugin_base import BootstrapPluginBase


class CardChildBase(BootstrapPluginBase):
    require_parent = True
    parent_classes = ['BootstrapCardPlugin']
    allow_children = True
    render_template = 'cascade/generic/wrapper.html'
    child_classes = ['BootstrapCardHeaderPlugin', 'BootstrapCardBodyPlugin', 'BootstrapCardFooterPlugin']


class BootstrapCardHeaderPlugin(TransparentContainer, CardChildBase):
    name = _("Card Header")
    default_css_class = 'card-header'

plugin_pool.register_plugin(BootstrapCardHeaderPlugin)


class BootstrapCardBodyPlugin(TransparentContainer, CardChildBase):
    name = _("Card Body")
    default_css_class = 'card-body'

plugin_pool.register_plugin(BootstrapCardBodyPlugin)


class BootstrapCardFooterPlugin(TransparentContainer, CardChildBase):
    name = _("Card Footer")
    default_css_class = 'card-footer'

plugin_pool.register_plugin(BootstrapCardFooterPlugin)


class BootstrapCardPlugin(TransparentWrapper, BootstrapPluginBase):
    """
    Use this plugin to display a card with optional card-header and card-footer.
    """
    name = _("Card")
    default_css_class = 'card'
    require_parent = False
    parent_classes = ['BootstrapColumnPlugin']
    allow_children = True
    render_template = 'cascade/bootstrap4/card.html'

    @classmethod
    def get_identifier(cls, instance):
        try:
            return instance.card_header or instance.card_footer
        except AttributeError:
            pass
        return super().get_identifier(instance)

    @classmethod
    def get_child_classes(cls, slot, page, instance=None):
        """Restrict child classes of Card to one of each: Header, Body and Footer"""
        child_classes = super().get_child_classes(slot, page, instance)
        # allow only one child of type Header, Body, Footer
        for child in instance.get_children():
            if child.plugin_type in child_classes:
                child_classes.remove(child.plugin_type)
        return child_classes

plugin_pool.register_plugin(BootstrapCardPlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/carousel.py
================================================
import re
import logging
from django.forms import widgets
from django.forms.fields import IntegerField, MultipleChoiceField
from django.utils.safestring import mark_safe
from django.utils.translation import ngettext_lazy, gettext_lazy as _

from entangled.forms import EntangledModelFormMixin
from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.bootstrap4.fields import BootstrapMultiSizeField
from cmsplugin_cascade.bootstrap4.grid import Breakpoint
from cmsplugin_cascade.bootstrap4.picture import get_picture_elements
from cmsplugin_cascade.bootstrap4.plugin_base import BootstrapPluginBase
from cmsplugin_cascade.bootstrap4.utils import IMAGE_RESIZE_OPTIONS
from cmsplugin_cascade.forms import ManageChildrenFormMixin
from cmsplugin_cascade.image import ImagePropertyMixin, ImageFormMixin

logger = logging.getLogger('cascade')


class CarouselSlidesFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin):
    OPTION_CHOICES = [('slide', _("Animate")), ('pause', _("Pause")), ('wrap', _("Wrap"))]

    num_children = IntegerField(min_value=1, initial=1,
        label=_('Slides'),
        help_text=_('Number of slides for this carousel.'),
    )

    interval = IntegerField(
        label=_("Interval"),
        initial=5,
        help_text=_("Change slide after this number of seconds."),
    )

    options = MultipleChoiceField(
        label=_('Options'),
        choices=OPTION_CHOICES,
        widget=widgets.CheckboxSelectMultiple,
        initial=['slide', 'wrap', 'pause'],
        help_text=_("Adjust interval for the carousel."),
    )

    container_max_heights = BootstrapMultiSizeField(
        label=_("Carousel heights"),
        allowed_units=['px'],
        initial=['100px', '150px', '200px', '250px', '300px'],
        help_text=_("Heights of Carousel in pixels for distinct Bootstrap's breakpoints."),
    )

    resize_options = MultipleChoiceField(
        label=_("Resize Options"),
        choices=IMAGE_RESIZE_OPTIONS,
        widget=widgets.CheckboxSelectMultiple,
        help_text=_("Options to use when resizing the image."),
        initial=['upscale', 'crop', 'subject_location', 'high_resolution'],
    )

    class Meta:
        untangled_fields = ['num_children']
        entangled_fields = {'glossary': ['interval', 'options', 'container_max_heights', 'resize_options']}


class BootstrapCarouselPlugin(BootstrapPluginBase):
    name = _("Carousel")
    default_css_class = 'carousel'
    default_css_attributes = ['options']
    parent_classes = ['BootstrapColumnPlugin']
    render_template = 'cascade/bootstrap4/{}carousel.html'
    default_inline_styles = {'overflow': 'hidden'}
    form = CarouselSlidesFormMixin
    DEFAULT_CAROUSEL_ATTRIBUTES = {'data-ride': 'carousel'}

    @classmethod
    def get_identifier(cls, obj):
        num_cols = obj.get_num_children()
        content = ngettext_lazy('with {0} slide', 'with {0} slides', num_cols).format(num_cols)
        return mark_safe(content)

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = cls.super(BootstrapCarouselPlugin, cls).get_css_classes(obj)
        if 'slide' in obj.glossary.get('options', []):
            css_classes.append('slide')
        return css_classes

    @classmethod
    def get_html_tag_attributes(cls, obj):
        attributes = cls.super(BootstrapCarouselPlugin, cls).get_html_tag_attributes(obj)
        attributes.update(cls.DEFAULT_CAROUSEL_ATTRIBUTES)
        attributes['data-interval'] = 1000 * int(obj.glossary.get('interval', 5))
        options = obj.glossary.get('options', [])
        attributes['data-pause'] = 'pause' in options and 'hover' or 'false'
        attributes['data-wrap'] = 'wrap' in options and 'true' or 'false'
        return attributes

    def save_model(self, request, obj, form, change):
        wanted_children = int(form.cleaned_data.get('num_children'))
        super().save_model(request, obj, form, change)
        self.extend_children(obj, wanted_children, BootstrapCarouselSlidePlugin)
        obj.sanitize_children()

    @classmethod
    def sanitize_model(cls, obj):
        sanitized = super().sanitize_model(obj)
        complete_glossary = obj.get_complete_glossary()
        # fill all invalid heights for this container to a meaningful value
        max_height = max(obj.glossary['container_max_heights'].values())
        pattern = re.compile(r'^(\d+)px$')
        for bp in complete_glossary.get('breakpoints', ()):
            if not pattern.match(obj.glossary['container_max_heights'].get(bp, '')):
                obj.glossary['container_max_heights'][bp] = max_height
        return sanitized

plugin_pool.register_plugin(BootstrapCarouselPlugin)


class BootstrapCarouselSlidePlugin(BootstrapPluginBase):
    name = _("Slide")
    model_mixins = (ImagePropertyMixin,)
    default_css_class = 'img-fluid'
    parent_classes = ['BootstrapCarouselPlugin']
    raw_id_fields = ['image_file']
    html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'}
    render_template = 'cascade/bootstrap4/carousel-slide.html'
    form = ImageFormMixin
    alien_child_classes = True

    def render(self, context, instance, placeholder):
        context = self.super(BootstrapCarouselSlidePlugin, self).render(context, instance, placeholder)
        # slide image shall be rendered in a responsive context using the ``<picture>`` element
        try:
            parent_glossary = instance.parent.get_bound_plugin().glossary
            instance.glossary.update(responsive_heights=parent_glossary['container_max_heights'])
            elements = get_picture_elements(instance)
        except Exception as exc:
            logger.warning("Unable generate picture elements. Reason: {}".format(exc))
        else:
            context.update({
                'is_fluid': False,
                'elements': elements,
            })
        return context

    @classmethod
    def sanitize_model(cls, obj):
        sanitized = super().sanitize_model(obj)
        resize_options = obj.get_parent_glossary().get('resize_options', [])
        if obj.glossary.get('resize_options') != resize_options:
            obj.glossary.update(resize_options=resize_options)
            sanitized = True
        parent = obj.parent
        while parent.plugin_type != 'BootstrapColumnPlugin':
            parent = parent.parent
            if parent is None:
                logger.warning("PicturePlugin(pk={}) has no ColumnPlugin as ancestor.".format(obj.pk))
                return
        grid_column = parent.get_bound_plugin().get_grid_instance()
        obj.glossary.setdefault('media_queries', {})
        for bp in Breakpoint:
            obj.glossary['media_queries'].setdefault(bp.name, {})
            width = round(grid_column.get_bound(bp).max)
            if obj.glossary['media_queries'][bp.name].get('width') != width:
                obj.glossary['media_queries'][bp.name]['width'] = width
                sanitized = True
            if obj.glossary['media_queries'][bp.name].get('media') != bp.media_query:
                obj.glossary['media_queries'][bp.name]['media'] = bp.media_query
                sanitized = True
        return sanitized

    @classmethod
    def get_identifier(cls, obj):
        try:
            content = obj.image.name or obj.image.original_filename
        except AttributeError:
            content = _("Empty Slide")
        return mark_safe(content)

plugin_pool.register_plugin(BootstrapCarouselSlidePlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/container.py
================================================
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms import widgets
from django.forms.fields import BooleanField, ChoiceField, MultipleChoiceField
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
from django.utils.translation import ngettext_lazy, gettext_lazy as _

from cms.plugin_pool import plugin_pool
from entangled.forms import EntangledModelFormMixin
from cmsplugin_cascade import app_settings
from cmsplugin_cascade.bootstrap4.grid import Breakpoint
from cmsplugin_cascade.forms import ManageChildrenFormMixin
from .plugin_base import BootstrapPluginBase
from . import grid


def get_widget_choices():
    breakpoints = app_settings.CMSPLUGIN_CASCADE['bootstrap4']['fluid_bounds']
    widget_choices = []
    for index, (bp, bound) in enumerate(breakpoints.items()):
        if index == 0:
            widget_choices.append((bp.name, "{} (<{:.1f}px)".format(bp.label, bound.max)))
        elif index == len(breakpoints) - 1:
            widget_choices.append((bp.name, "{} (≥{:.1f}px)".format(bp.label, bound.min)))
        else:
            widget_choices.append((bp.name, "{} (≥{:.1f}px and <{:.1f}px)".format(bp.label, bound.min, bound.max)))
    return widget_choices


class ContainerBreakpointsWidget(widgets.CheckboxSelectMultiple):
    template_name = 'cascade/admin/widgets/container_breakpoints.html'


class ContainerFormMixin(EntangledModelFormMixin):
    breakpoints = MultipleChoiceField(
        label=_('Available Breakpoints'),
        choices=get_widget_choices(),
        widget=ContainerBreakpointsWidget(choices=get_widget_choices()),
        initial=[bp.name for bp in app_settings.CMSPLUGIN_CASCADE['bootstrap4']['fluid_bounds'].keys()],
        help_text=_("Supported display widths for Bootstrap's grid system."),
    )

    fluid = BooleanField(
        label=_('Fluid Container'),
        initial=False,
        required=False,
        help_text=_("Changing your outermost '.container' to '.container-fluid'.")
    )

    class Meta:
        entangled_fields = {'glossary': ['breakpoints', 'fluid']}

    def clean_breapoints(self):
        # TODO: check this
        if len(self.cleaned_data['glossary']['breakpoints']) == 0:
            raise ValidationError(_("At least one breakpoint must be selected."))
        return self.cleaned_data['glossary']


class ContainerGridMixin:
    def get_grid_instance(self):
        fluid = self.glossary.get('fluid', False)
        try:
            breakpoints = [getattr(grid.Breakpoint, bp) for bp in self.glossary['breakpoints']]
        except KeyError:
            breakpoints = [bp for bp in grid.Breakpoint]
        if fluid:
            bounds = dict((bp, grid.fluid_bounds[bp]) for bp in breakpoints)
        else:
            bounds = dict((bp, grid.default_bounds[bp]) for bp in breakpoints)
        return grid.Bootstrap4Container(bounds=bounds)


class BootstrapContainerPlugin(BootstrapPluginBase):
    name = _("Container")
    parent_classes = None
    require_parent = False
    model_mixins = (ContainerGridMixin,)
    form = ContainerFormMixin
    footnote_html = """<p>
    For more information about the Container please read the
    <a href="https://getbootstrap.com/docs/4.3/layout/overview/#containers" target="_new">Bootstrap documentation</a>.
    </p>"""

    @classmethod
    def get_identifier(cls, obj):
        breakpoints = obj.glossary.get('breakpoints')
        content = obj.glossary.get('fluid') and '(fluid) ' or ''
        if breakpoints:
            BREAKPOINTS = app_settings.CMSPLUGIN_CASCADE['bootstrap4']['fluid_bounds']
            devices = ', '.join([str(bp.label) for bp in BREAKPOINTS if bp.name in breakpoints])
            content = _("{0}for {1}").format(content, devices)
        return mark_safe(content)

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = cls.super(BootstrapContainerPlugin, cls).get_css_classes(obj)
        if obj.glossary.get('fluid'):
            css_classes.append('container-fluid')
        else:
            css_classes.append('container')
        return css_classes

    def save_model(self, request, obj, form, change):
        super().save_model(request, obj, form, change)
        obj.sanitize_children()

plugin_pool.register_plugin(BootstrapContainerPlugin)


class BootstrapRowFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin):
    """
    Form class to add non-materialized field to count the number of children.
    """
    ROW_NUM_COLUMNS = [1, 2, 3, 4, 6, 12]
    num_children = ChoiceField(
        label=_('Columns'),
        choices=[(i, ngettext_lazy('{0} column', '{0} columns', i).format(i)) for i in ROW_NUM_COLUMNS],
        initial=3,
        help_text=_('Number of columns to be created with this row.'),
    )

    class Meta:
        untangled_fields = ['num_children']


class RowGridMixin:
    def get_grid_instance(self):
        row = grid.Bootstrap4Row()
        query = Q(plugin_type='BootstrapContainerPlugin') | Q(plugin_type='BootstrapColumnPlugin') \
          | Q(plugin_type='BootstrapJumbotronPlugin')
        container = self.get_ancestors().order_by('depth').filter(query).last().get_bound_plugin().get_grid_instance()
        container.add_row(row)
        return row


class BootstrapRowPlugin(BootstrapPluginBase):
    name = _("Row")
    default_css_class = 'row'
    parent_classes = ['BootstrapContainerPlugin', 'BootstrapColumnPlugin', 'BootstrapJumbotronPlugin']
    model_mixins = (RowGridMixin,)
    form = BootstrapRowFormMixin

    @classmethod
    def get_identifier(cls, obj):
        num_cols = obj.get_num_children()
        content = ngettext_lazy("with {0} column", "with {0} columns", num_cols).format(num_cols)
        return mark_safe(content)

    def save_model(self, request, obj, form, change):
        wanted_children = int(form.cleaned_data.get('num_children'))
        super().save_model(request, obj, form, change)
        child_glossary = {'xs-column-width': 'col'}
        self.extend_children(obj, wanted_children, BootstrapColumnPlugin, child_glossary=child_glossary)

plugin_pool.register_plugin(BootstrapRowPlugin)


class ColumnGridMixin:
    valid_keys = ['xs-column-width', 'sm-column-width', 'md-column-width', 'lg-column-width', 'xs-column-width',
                  'xs-column-offset', 'sm-column-offset', 'md-column-offset', 'lg-column-offset', 'xs-column-offset']
    def get_grid_instance(self):
        column = None
        query = Q(plugin_type='BootstrapRowPlugin')
        row_obj = self.get_ancestors().order_by('depth').filter(query).last().get_bound_plugin()
        # column_siblings = row_obj.get_descendants().order_by('depth').filter(plugin_type='BootstrapColumnPlugin')
        row = row_obj.get_grid_instance()
        for column_sibling in self.get_siblings():
            classes = [val for key, val in column_sibling.get_bound_plugin().glossary.items()
                       if key in self.valid_keys and val]
            if column_sibling.pk == self.pk:
                column = grid.Bootstrap4Column(classes)
                row.add_column(column)
            else:
                row.add_column(grid.Bootstrap4Column(classes))
        return column


class BootstrapColumnPlugin(BootstrapPluginBase):
    name = _("Column")
    parent_classes = ['BootstrapRowPlugin']
    child_classes = ['BootstrapJumbotronPlugin']
    alien_child_classes = True
    default_css_attributes = [fmt.format(bp.name) for bp in grid.Breakpoint
        for fmt in ('{}-column-width', '{}-column-offset', '{}-column-ordering', '{}-responsive-utils')]
    model_mixins = (ColumnGridMixin,)

    def get_form(self, request, obj=None, **kwargs):
        def choose_help_text(*phrases):
            bounds = 'fluid_bounds' if container.glossary.get('fluid') else 'default_bounds'
            bs4_breakpoints = app_settings.CMSPLUGIN_CASCADE['bootstrap4'][bounds]
            if last:
                return phrases[0].format(bs4_breakpoints[last].max)
            elif len(breakpoints) > 1:
                return phrases[1].format(bs4_breakpoints[first].min)
            else:
                return phrases[2]

        if 'parent' in self._cms_initial_attributes:
            container=self._cms_initial_attributes['parent'].get_ancestors().order_by('depth').last().get_bound_plugin()
        else:
            containers=obj.get_ancestors().filter(plugin_type='BootstrapContainerPlugin')
            if containers:
                container=containers.order_by('depth').last().get_bound_plugin()
            else:
                jumbotrons=obj.get_ancestors().filter(plugin_type='BootstrapJumbotronPlugin')
                container=jumbotrons.order_by('depth').last().get_bound_plugin()
        breakpoints = container.glossary['breakpoints']

        width_fields, offset_fields, reorder_fields, responsive_fields = {}, {}, {}, {}
        units = [ngettext_lazy("{} unit", "{} units", i).format(i) for i in range(0, 13)]
        previous_devices, previous_label = '', ''
        for bp in breakpoints:
            try:
                last = getattr(grid.Breakpoint, breakpoints[breakpoints.index(bp)])
            except IndexError:
                last = None
            finally:
                first = getattr(grid.Breakpoint, bp)
                devices = ', '.join([str(b.label) for b in grid.Breakpoint.range(first, last)])

            if bp == 'xs':
                choices = [('col', _("Flex column"))]
                choices.extend(('col-{}'.format(i), _("{} fixed column").format(units[i])) for i in range(1, 13))
                choices.append(('col-auto', _("Auto column")))
            else:
                choices = [('col-{}'.format(bp), _("Flex column"))]
                choices.extend(('col-{}-{}'.format(bp, i), _("{} fixed column").format(units[i])) for i in range(1, 13))
                choices.append(('col-{}-auto'.format(bp), _("Auto column")))
            if breakpoints.index(bp) == 0:
                # first breakpoint
                field_name = '{}-column-width'.format(bp)
                width_fields[field_name] = ChoiceField(
                    choices=choices,
                    label=_("Column width for {}").format(devices),
                    initial='col' if bp == 'xs' else 'col-{}'.format(bp),
                    help_text=choose_help_text(
                        _("Column width for devices narrower than {:.1f} pixels."),
                        _("Column width for devices wider than {:.1f} pixels."),
                        _("Column width for all devices."),
                    )
                )
            else:
                # wider breakpoints may inherit from next narrower ones
                choices.insert(0, ('', format_lazy(_("Inherit column width from {}"), previous_devices)))
                field_name = '{}-column-width'.format(bp)
                width_fields[field_name] = ChoiceField(
                    choices=choices,
                    label=_("Column width for {}").format(devices),
                    initial='',
                    required=False,
                    help_text=choose_help_text(
                        _("Override column width for devices narrower than {:.1f} pixels."),
                        _("Override column width for devices wider than {:.1f} pixels."),
                        _("Override column width for all devices."),
                    )
                )
            previous_devices = devices

            # handle offset
            if breakpoints.index(bp) == 0:
                choices = [('', _("No offset"))]
                offset_range = range(1, 13)
            else:
                choices = [('', format_lazy(_("Inherit offset from {}"), previous_label))]
                offset_range = range(0, 13)
            previous_label = Breakpoint[bp].label
            if bp == 'xs':
                choices.extend(('offset-{}'.format(i), units[i]) for i in offset_range)
            else:
                choices.extend(('offset-{}-{}'.format(bp, i), units[i]) for i in offset_range)
            label = _("Offset for {}").format(devices)
            help_text = choose_help_text(
                _("Offset width for devices narrower than {:.1f} pixels."),
                _("Offset width for devices wider than {:.1f} pixels."),
                _("Offset width for all devices.")
            )
            field_name = '{}-column-offset'.format(bp)
            offset_fields[field_name] = ChoiceField(
                choices=choices,
                label=label,
                required=False,
                help_text=help_text,
            )

            # handle column reordering
            choices = [('', _("No reordering"))]
            if bp == 'xs':
                choices.extend(('order-{}'.format(i), _("Reorder by {}").format(units[i])) for i in range(1, 13))
            else:
                choices.extend(('order-{}-{}'.format(bp, i), _("Reorder by {}").format(units[i])) for i in range(1, 13))
            label = _("Reordering for {}").format(devices)
            help_text = choose_help_text(
                _("Reordering for devices narrower than {:.1f} pixels."),
                _("Reordering for devices wider than {:.1f} pixels."),
                _("Reordering for all devices.")
            )
            field_name = '{}-column-ordering'.format(bp)
            reorder_fields[field_name] = ChoiceField(
                choices=choices,
                label=label,
                required=False,
                help_text=help_text,
            )

            # handle responsive utilities
            choices = [('', _("Default")), ('visible-{}'.format(bp), _("Visible")), ('hidden-{}'.format(bp), _("Hidden"))]
            label = _("Responsive utilities for {}").format(devices)
            help_text = choose_help_text(
                _("Utility classes for showing and hiding content by devices narrower than {:.1f} pixels."),
                _("Utility classes for showing and hiding content by devices wider than {:.1f} pixels."),
                _("Utility classes for showing and hiding content for all devices.")
            )
            field_name = '{}-responsive-utils'.format(bp)
            responsive_fields[field_name] = ChoiceField(
                choices=choices,
                label=label,
                initial='',
                widget=widgets.RadioSelect,
                required=False,
                help_text=help_text,
            )
        glossary_fields = list(width_fields.keys())
        glossary_fields.extend(offset_fields.keys())
        glossary_fields.extend(reorder_fields.keys())
        glossary_fields.extend(responsive_fields.keys())

        class Meta:
            entangled_fields = {'glossary': glossary_fields}

        attrs = dict(width_fields, **offset_fields, **reorder_fields, **responsive_fields, Meta=Meta)
        kwargs['form'] = type('ColumnForm', (EntangledModelFormMixin,), attrs)
        return super().get_form(request, obj, **kwargs)

    def save_model(self, request, obj, form, change):
        super().save_model(request, obj, form, change)
        obj.sanitize_children()

    @classmethod
    def sanitize_model(cls, obj):
        sanitized = super().sanitize_model(obj)
        return sanitized

    @classmethod
    def get_identifier(cls, obj):
        glossary = obj.get_complete_glossary()
        widths = []
        for bp in glossary.get('breakpoints', []):
            width = obj.glossary.get('{0}-column-width'.format(bp), '').replace('col-{0}-'.format(bp), '')
            if width:
                widths.append(width)
        if len(widths) > 0:
            content = _("widths: {}").format(' / '.join(widths))
        else:
            content = _("unknown width")
        return mark_safe(content)

plugin_pool.register_plugin(BootstrapColumnPlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/embeds.py
================================================
import re
from urllib.parse import urlparse, urlunparse, ParseResult

from django.core.exceptions import ValidationError
from django.forms import widgets
from django.forms.fields import BooleanField, ChoiceField, URLField
from django.utils.translation import gettext_lazy as _
from entangled.forms import EntangledModelFormMixin, EntangledField
from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.bootstrap4.plugin_base import BootstrapPluginBase


class YoutubeFormMixin(EntangledModelFormMixin):
    ASPECT_RATIO_CHOICES = [
        ('embed-responsive-21by9', _("Responsive 21:9")),
        ('embed-responsive-16by9', _("Responsive 16:9")),
        ('embed-responsive-4by3', _("Responsive 4:3")),
        ('embed-responsive-1by1', _("Responsive 1:1")),
    ]

    videoid = EntangledField()

    url = URLField(
        label=_("YouTube URL"),
        widget=widgets.URLInput(attrs={'size': 50}),
    )

    aspect_ratio = ChoiceField(
        label=_("Aspect Ratio"),
        choices=ASPECT_RATIO_CHOICES,
        widget=widgets.RadioSelect,
        initial=ASPECT_RATIO_CHOICES[1][0],
    )

    allow_fullscreen = BooleanField(
        label=_("Allow Fullscreen"),
        required=False,
        initial=True,
    )

    autoplay = BooleanField(
        label=_("Autoplay"),
        required=False,
    )

    controls = BooleanField(
        label=_("Display Controls"),
        required=False,
    )

    loop = BooleanField(
        label=_("Enable Looping"),
        required=False,
    )

    rel = BooleanField(
        label=_("Show related"),
        required=False,
        help_text=_("Show videos suggested by YouTube at the end."),
    )

    class Meta:
        untangled_fields = ['url']
        entangled_fields = {'glossary': ['videoid', 'aspect_ratio', 'allow_fullscreen', 'autoplay',
                                         'controls', 'loop', 'rel']}

    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance')
        if instance:
            videoid = instance.glossary.get('videoid')
            if videoid:
                parts = ParseResult('https', 'youtu.be', videoid, '', '', '')
                initial = {'url': urlunparse(parts)}
                kwargs.update(initial=initial)
        super().__init__(*args, **kwargs)

    def clean(self):
        cleaned_data = super().clean()
        url = cleaned_data.get('url')
        if url:
            parts = urlparse(url)
            match = re.search(r'^v=([^&]+)', parts.query)
            if match:
                cleaned_data['videoid'] = match.group(1)
                return cleaned_data
            match = re.search(r'([^/]+)$', parts.path)
            if match:
                cleaned_data['videoid'] = match.group(1)
                return cleaned_data
        raise ValidationError(_("Please enter a valid YouTube URL"))


class BootstrapYoutubePlugin(BootstrapPluginBase):
    """
    Use this plugin to display a YouTube video.
    """
    name = _("You Tube")
    require_parent = False
    parent_classes = ['BootstrapColumnPlugin']
    child_classes = None
    render_template = 'cascade/bootstrap4/youtube.html'
    form = YoutubeFormMixin

    def render(self, context, instance, placeholder):
        context = self.super(BootstrapYoutubePlugin, self).render(context, instance, placeholder)
        query_params = ['autoplay', 'controls', 'loop', 'rel']
        videoid = instance.glossary.get('videoid')
        if videoid:
            query = ['{}=1'.format(key) for key in query_params if instance.glossary.get(key)]
            parts = ParseResult('https', 'www.youtube.com', '/embed/' + videoid, '', '&'.join(query), '')
            context.update({
                'youtube_url': urlunparse(parts),
                'allowfullscreen': 'allowfullscreen' if instance.glossary.get('allow_fullscreen') else '',
            })
        return context

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = cls.super(BootstrapYoutubePlugin, cls).get_css_classes(obj)
        css_classes.append('embed-responsive')
        css_class = obj.glossary.get('aspect_ratio')
        if css_class:
            css_classes.append(css_class)
        return css_classes

    @classmethod
    def get_identifier(cls, obj):
        return obj.glossary.get('videoid', '')

plugin_pool.register_plugin(BootstrapYoutubePlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/fields.py
================================================
from cmsplugin_cascade.bootstrap4.grid import Breakpoint
from cmsplugin_cascade.fields import MultiSizeField


class BootstrapMultiSizeField(MultiSizeField):
    """
    Some size input fields must be specified per Bootstrap breakpoint. Use this multiple
    input field to handle this.
    """
    def __init__(self, *args, **kwargs):
        properties = [bp.name for bp in Breakpoint]
        kwargs['sublabels'] = [bp.label for bp in Breakpoint]
        super().__init__(properties, *args, **kwargs)


================================================
FILE: cmsplugin_cascade/bootstrap4/grid.py
================================================
from copy import copy
from enum import Enum, unique
from functools import reduce
import itertools
from operator import add
import re

from django.utils.translation import gettext_lazy as _


class BootstrapException(Exception):
    """
    Raised if arrangement of Bootstrap-4 elements would render weird results.
    """


@unique
class Breakpoint(Enum):
    """
    Enumerate the five breakpoints defined by the Bootstrap-4 CSS framework.
    """
    xs = 0
    sm = 1
    md = 2
    lg = 3
    xl = 4

    @classmethod
    def range(cls, first, last):
        """
        Iterate over all elements, starting from `first` until `last`.
        The last element is included.
        """
        if first: first = first.value
        if last: last = last.value + 1
        return itertools.islice(cls, first, last)

    def __gt__(self, other):
        return self.value > other.value

    def __ge__(self, other):
        return self.value >= other.value

    def __lt__(self, other):
        return self.value < other.value

    def __le__(self, other):
        return self.value <= other.value

    def __iter__(self):
        yield self.xs
        yield self.sm
        yield self.md
        yield self.lg
        yield self.xl

    @property
    def label(self):
        return [
            _("Portrait Phones"),
            _("Landscape Phones"),
            _("Tablets"),
            _("Laptops"),
            _("Large Desktops"),
        ][self.value]

    @property
    def media_query(self):
        return [
            '(max-width: 575.98px)',
            '(min-width: 576px) and (max-width: 767.98px)',
            '(min-width: 768px) and (max-width: 991.98px)',
            '(min-width: 992px) and (max-width: 1199.98px)',
            '(min-width: 1200px)',
        ][self.value]


class Bound:
    def __init__(self, min, max):
        self.min = float(min)
        self.max = float(max)

    def __copy__(self):
        return type(self)(self.min, self.max)

    def __eq__(self, other):
        return round(self.min, 1) == round(other.min, 1) and round(self.max, 1) == round(other.max, 1)

    def __add__(self, other):
        return Bound(
            self.min + other.min,
            self.max + other.max,
        )

    def __sub__(self, other):
        return Bound(
            self.min - other.min,
            self.max - other.max,
        )

    def __repr__(self):
        return "<{}: min={}, max={}>".format(self.__class__.__name__, self.min, self.max)

    def extend(self, other):
        self.min = min(self.min, other.min)
        self.max = max(self.max, other.max)


default_bounds = {
    Breakpoint.xs: Bound(320, 572),
    Breakpoint.sm: Bound(540, 540),
    Breakpoint.md: Bound(720, 720),
    Breakpoint.lg: Bound(960, 960),
    Breakpoint.xl: Bound(1140, 1140),
}

fluid_bounds = {
    Breakpoint.xs: Bound(320, 576),
    Breakpoint.sm: Bound(576, 768),
    Breakpoint.md: Bound(768, 992),
    Breakpoint.lg: Bound(992, 1200),
    Breakpoint.xl: Bound(1200, 1980),
}


class Break:
    def __init__(self, breakpoint, classes, narrower=None):
        self.breakpoint = breakpoint
        self.fixed_units = 0
        self.flex_column = False
        self.auto_column = False
        self._normalize_col_classes(classes)
        if isinstance(narrower, Break):
            self._inherit_from(narrower)
        self.bound = None

    def _normalize_col_classes(self, classes):
        if self.breakpoint == Breakpoint.xs:
            fixed_pat = re.compile(r'^col-(\d+)$')
            flex_pat = re.compile(r'^col$')
            auto_pat = re.compile(r'^col-auto$')
        else:
            fixed_pat = re.compile(r'^col-{}-(\d+)$'.format(self.breakpoint.name))
            flex_pat = re.compile(r'^col-{}$'.format(self.breakpoint.name))
            auto_pat = re.compile(r'^col-{}-auto$'.format(self.breakpoint.name))
        for col_class in classes:
            # look for CSS classes matching fixed size columns
            fixed = fixed_pat.match(col_class)
            if fixed:
                if self.fixed_units or self.flex_column or self.auto_column:
                    raise BootstrapException("Can not mix fixed- with flex- or auto-column")
                units = int(fixed.group(1))
                if units < 1 or units > 12:
                    raise BootstrapException("Column units value {} out of range".format(units))
                self.fixed_units = units

            # look for CSS classes matching flex columns
            flex = flex_pat.match(col_class)
            if flex:
                if self.fixed_units or self.flex_column or self.auto_column:
                    raise BootstrapException("Can not mix flex- with fixed- or auto-column")
                self.flex_column = True

            # look for CSS classes matching auto columns
            auto = auto_pat.match(col_class)
            if auto:
                if self.fixed_units or self.flex_column or self.auto_column:
                    raise BootstrapException("Can not mix auto- with fixed- or flex-column")
                self.auto_column = True

    def _inherit_from(self, narrower):
        if self.breakpoint <= narrower.breakpoint:
            raise BootstrapException("Can only inherit column bounds from narrower breakpoint")
        if self.fixed_units == 0 and self.flex_column is False and self.auto_column is False:
            self.fixed_units = narrower.fixed_units
            self.flex_column = narrower.flex_column
            self.auto_column = narrower.auto_column

    def __copy__(self):
        newone = type(self)()
        if self.bound:
            newone.bound = dict(self.bound)
        return newone

    def __repr__(self):
        return "<{}[{}]: fixed={}, flex={}, auto={}>".format(
            self.__class__.__name__, self.breakpoint.name, self.fixed_units, self.flex_column, self.auto_column)


class Bootstrap4Container(list):
    """
    Abstracts a Bootstrap-4 container element, such as ``<div class="container">...</div>``, so that the minimum and
    maximum widths each each child row can be computed.
    Each container object is a list of one to many ``Bootstrap4Row`` instances.
    In order to model a "fluid" container, use ``fluid_bounds`` during construction.
    """
    # def __init__(self, bounds=app_settings.CMSPLUGIN_CASCADE['bootstrap4']['default_bounds']):
    def __init__(self, bounds=default_bounds):
            self.bounds = bounds

    def __repr__(self):
        return "<{}: {}>".format(self.__class__.__name__, ', '.join([repr(o) for o in self]))

    def add_row(self, row):
        if isinstance(row.parent, (Bootstrap4Container, Bootstrap4Column)):
            # detach from previous container or column
            pos = row.parent.index(row)
            row.parent.pop(pos)
        row.parent = self
        row.bounds = dict(self.bounds)
        self.append(row)
        return row


class Bootstrap4Row(list):
    """
    Abstracts a Bootstrap-4 row element, such as ``<div class="row">...</div>``, so that the minimum and maximum widths
    each each child columns can be computed.
    Each row object is a list of 1 to many ``Bootstrap4Column`` instances.
    """
    parent = None
    bounds = None

    def __repr__(self):
        return "<{}: {}>".format(self.__class__.__name__, ', '.join([repr(o) for o in self]))

    def add_column(self, column):
        if isinstance(column.parent, Bootstrap4Row):
            pos = column.parent.index(column)
            column.parent.pop(pos)
        column.parent = self
        self.append(column)

    def compute_column_bounds(self):
        assert isinstance(self.bounds, dict), "Expected `bounds` to be a dict."
        for bp in [Breakpoint.xs, Breakpoint.sm, Breakpoint.md, Breakpoint.lg, Breakpoint.xl]:
            if bp in self.bounds:
                remaining_width = copy(self.bounds[bp])

            # first compute the bounds of columns with a fixed width
            for column in self:
                if column.breaks[bp].fixed_units:
                    assert column.breaks[bp].bound is None, "Expected `column.breaks[bp].bound` to be None."
                    column.breaks[bp].bound = Bound(
                        column.breaks[bp].fixed_units * self.bounds[bp].min / 12,
                        column.breaks[bp].fixed_units * self.bounds[bp].max / 12,
                    )
                    remaining_width -= column.breaks[bp].bound

            flex_columns = reduce(add, [int(col.breaks[bp].flex_column) for col in self], 0)
            auto_columns = reduce(add, [int(col.breaks[bp].auto_column) for col in self], 0)
            if auto_columns:
                # we use auto-columns, therefore estimate the min- and max values
                for column in self:
                    if column.breaks[bp].flex_column or column.breaks[bp].auto_column:
                        assert column.breaks[bp].bound is None, "Expected `column.breaks[bp].bound` to be None."
                        column.breaks[bp].bound = Bound(
                            30,
                            remaining_width.max - 30 * (flex_columns + auto_columns),
                        )
            else:
                # we use flex-columns exclusively, therefore subdivide the remaining width
                for column in self:
                    if column.breaks[bp].flex_column:
                        assert column.breaks[bp].bound is None, "Expected `column.breaks[bp].bound` to be None."
                        column.breaks[bp].bound = Bound(
                            remaining_width.min / flex_columns,
                            remaining_width.max / flex_columns,
                        )


class Bootstrap4Column(list):
    """
    Abstracts a Bootstrap-4 column element, such as ``<div class="col...">...</div>``, which shall be added to a
    ``Bootstrap4Row`` element.
    Each column may contain elements of type ``Bootstrap4Row``.
    For each breakpoint, a columns knows its extensions.
    """
    parent = None

    def __init__(self, classes=[]):
        if isinstance(classes, str):
            classes = classes.split()
        narrower = None
        self.breaks = {}
        for bp in Breakpoint:
            self.breaks[bp] = Break(bp, classes, narrower)
            narrower = self.breaks[bp]

    def __repr__(self):
        return "<{}: {}>".format(self.__class__.__name__, ', '.join([repr(self.breaks[bp]) for bp in Breakpoint]))

    def __copy__(self):
        newone = type(self)()
        newone.__dict__.update(self.__dict__)
        return newone

    def add_row(self, row):
        if isinstance(row.parent, (Bootstrap4Container, Bootstrap4Column)):
            # detach from previous container or column
            pos = row.parent.index(row)
            row.parent.pop(pos)
            row.parent.bounds = None
        row.parent = self
        row.bounds = dict((bp, self.get_bound(bp)) for bp in Breakpoint)
        self.append(row)
        return row

    def get_bound(self, breakpoint):
        if self.breaks[breakpoint].bound is None:
            self.parent.compute_column_bounds()
            assert self.breaks[breakpoint].bound, "Expected `column.breaks[bp].bound` not to be None."
        return self.breaks[breakpoint].bound

    def get_min_max_bounds(self):
        """
        Return a dict of min- and max-values for the given column.
        This is required to estimate the bounds of images.
        """
        bound = Bound(999999.0, 0.0)
        for bp in Breakpoint:
            bound.extend(self.get_bound(bp))
        return {'min': bound.min, 'max': bound.max}


================================================
FILE: cmsplugin_cascade/bootstrap4/icon.py
================================================
from django.forms import ChoiceField
from django.utils.html import format_html, format_html_join
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.fields import SizeField, ColorField, BorderChoiceField
from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin
from cmsplugin_cascade.link.plugin_base import LinkElementMixin
from cmsplugin_cascade.icon.forms import IconFormMixin
from cmsplugin_cascade.icon.plugin_base import IconPluginMixin


class FramedIconFormMixin(IconFormMixin):
    SIZE_CHOICES = [('{}em'.format(c), "{} em".format(c)) for c in range(1, 13)]

    RADIUS_CHOICES = [(None, _("Square"))] + \
        [('{}px'.format(r), "{} px".format(r)) for r in (1, 2, 3, 5, 7, 10, 15, 20)] + \
        [('50%', _("Circle"))]

    TEXT_ALIGN_CHOICES = [
        (None, _("Do not align")),
        ('text-left', _("Left")),
        ('text-center', _("Center")),
        ('text-right', _("Right"))
    ]

    font_size = SizeField(
        label=_("Icon size"),
        allowed_units=['px', 'em'],
        initial='1em',
    )

    color = ColorField(
        label=_("Icon color"),
    )

    background_color = ColorField(
        label=_("Background color"),
        inherit_color=True,
    )

    text_align = ChoiceField(
        choices=TEXT_ALIGN_CHOICES,
        label=_("Text alignment"),
        required=False,
        help_text=_("Align the icon inside the parent column.")
    )

    border = BorderChoiceField(
        label=_("Set border"),
    )

    border_radius = ChoiceField(
        choices=RADIUS_CHOICES,
        label=_("Border radius"),
        required=False,
    )

    class Meta:
        entangled_fields = {'glossary': ['font_size', 'color', 'background_color', 'text_align', 'border',
                                         'border_radius']}


class FramedIconPlugin(IconPluginMixin, LinkPluginBase):
    name = _("Icon with frame")
    parent_classes = None
    require_parent = False
    allow_children = False
    render_template = 'cascade/bootstrap4/framedicon.html'
    model_mixins = (LinkElementMixin,)
    form = type('FramedIconForm', (LinkFormMixin, FramedIconFormMixin), {'require_link': False})
    ring_plugin = 'FramedIconPlugin'

    class Media:
        js = ['admin/js/jquery.init.js', 'cascade/js/admin/framediconplugin.js']

    @classmethod
    def get_tag_type(self, instance):
        if instance.glossary.get('text_align') or instance.glossary.get('font_size'):
            return 'div'

    @classmethod
    def get_css_classes(cls, instance):
        css_classes = cls.super(FramedIconPlugin, cls).get_css_classes(instance)
        text_align = instance.glossary.get('text_align')
        if text_align:
            css_classes.append(text_align)
        return css_classes

    @classmethod
    def get_inline_styles(cls, instance):
        inline_styles = cls.super(FramedIconPlugin, cls).get_inline_styles(instance)
        inline_styles['font-size'] = instance.glossary.get('font_size', '1em')
        return inline_styles

    def render(self, context, instance, placeholder):
        context = self.super(FramedIconPlugin, self).render(context, instance, placeholder)
        styles = {'display': 'inline-block'}
        color, inherit = instance.glossary.get('color', (ColorField.DEFAULT_COLOR, True))
        if not inherit:
            styles['color'] = color
        background_color, inherit = instance.glossary.get('background_color', (ColorField.DEFAULT_COLOR, True))
        if not inherit:
            styles['background-color'] = background_color
        border = instance.glossary.get('border')
        if isinstance(border, list) and border[0] and border[1] != 'none':
            styles.update(border='{0} {1} {2}'.format(*border))
            radius = instance.glossary.get('border_radius')
            if radius:
                styles['border-radius'] = radius
        attrs = []
        if 'icon_font_class' in context:
            attrs.append(format_html('class="{}"', context['icon_font_class']))
        attrs.append(format_html('style="{}"', format_html_join('', '{0}:{1};', [(k, v) for k, v in styles.items()])))
        context['icon_font_attrs'] = mark_safe(' '.join(attrs))
        return context

plugin_pool.register_plugin(FramedIconPlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/image.py
================================================
import logging
from django.forms import widgets, ChoiceField, MultipleChoiceField
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _

from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.bootstrap4.grid import Breakpoint
from cmsplugin_cascade.bootstrap4.utils import get_image_tags, IMAGE_RESIZE_OPTIONS, IMAGE_SHAPE_CHOICES
from cmsplugin_cascade.image import ImageFormMixin, ImagePropertyMixin
from cmsplugin_cascade.fields import SizeField
from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin
from cmsplugin_cascade.link.plugin_base import LinkElementMixin

logger = logging.getLogger('cascade.bootstrap4')


class BootstrapImageFormMixin(ImageFormMixin):
    ALIGNMENT_OPTIONS = [
        ('float-left', _("Left")),
        ('float-right', _("Right")),
        ('mx-auto', _("Center")),
    ]

    image_shapes = MultipleChoiceField(
        label=_("Image Shapes"),
        choices=IMAGE_SHAPE_CHOICES,
        widget=widgets.CheckboxSelectMultiple,
        initial=['img-fluid']
    )

    image_width_responsive = SizeField(
        label=_("Responsive Image Width"),
        allowed_units=['%'],
        initial='100%',
        required = False,
        help_text=_("Set the image width in percent relative to containing element."),
    )

    image_width_fixed = SizeField(
        label=_("Fixed Image Width"),
        allowed_units=['px'],
        required = False,
        help_text=_("Set a fixed image width in pixels."),
    )

    image_height = SizeField(
        label=_("Adapt Image Height"),
        allowed_units=['px', '%'],
        required = False,
        help_text=_("Set a fixed height in pixels, or percent relative to the image width."),
    )

    resize_options = MultipleChoiceField(
        label=_("Resize Options"),
        choices=IMAGE_RESIZE_OPTIONS,
        widget=widgets.CheckboxSelectMultiple,
        required = False,
        help_text=_("Options to use when resizing the image."),
        initial=['subject_location', 'high_resolution'],
    )

    image_alignment = ChoiceField(
        label=_("Image Alignment"),
        choices=ALIGNMENT_OPTIONS,
        widget=widgets.RadioSelect,
        required = False,
        help_text=_("How to align a non-responsive image."),
    )

    class Meta:
        entangled_fields = {'glossary': ['image_shapes', 'image_width_responsive', 'image_width_fixed',
                                         'image_height', 'resize_options', 'image_alignment']}


class BootstrapImagePlugin(LinkPluginBase):
    name = _("Image")
    module = 'Bootstrap'
    parent_classes = ['BootstrapColumnPlugin']
    require_parent = True
    allow_children = False
    raw_id_fields = LinkPluginBase.raw_id_fields + ['image_file']
    model_mixins = (ImagePropertyMixin, LinkElementMixin,)
    admin_preview = False
    ring_plugin = 'ImagePlugin'
    form = type('BootstrapImageForm', (LinkFormMixin, BootstrapImageFormMixin), {'require_link': False})
    render_template = 'cascade/bootstrap4/linked-image.html'
    default_css_attributes = ['image_shapes', 'image_alignment']
    html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'}
    html_tag_attributes.update(LinkPluginBase.html_tag_attributes)

    class Media:
        js = ['admin/js/jquery.init.js', 'cascade/js/admin/imageplugin.js']

    def render(self, context, instance, placeholder):
        context = self.super(BootstrapImagePlugin, self).render(context, instance, placeholder)
        try:
            image_tags = get_image_tags(instance)
        except Exception as exc:
            logger.warning("Unable generate image tags. Reason: {}".format(exc))
        else:
            extra_styles = image_tags.pop('extra_styles', None)
            if extra_styles:
                inline_styles = instance.glossary.get('inline_styles', {})
                inline_styles.update(extra_styles)
                instance.glossary['inline_styles'] = inline_styles
            context.update(dict(**image_tags))
        return context

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = cls.super(BootstrapImagePlugin, cls).get_css_classes(obj)
        css_class = obj.glossary.get('css_class')
        if css_class:
            css_classes.append(css_class)
        return css_classes

    @classmethod
    def get_identifier(cls, obj):
        try:
            content = str(obj.image)
        except AttributeError:
            content = _("No Image")
        return mark_safe(content)

    @classmethod
    def sanitize_model(cls, obj):
        sanitized = False
        parent = obj.parent
        try:
            while parent.plugin_type != 'BootstrapColumnPlugin':
                parent = parent.parent
            grid_column = parent.get_bound_plugin().get_grid_instance()
            min_max_bounds = grid_column.get_min_max_bounds()
            if obj.glossary.get('column_bounds') != min_max_bounds:
                obj.glossary['column_bounds'] = min_max_bounds
                sanitized = True
            obj.glossary.setdefault('media_queries', {})
            for bp in Breakpoint:
                media_query = '{} {:.2f}px'.format(bp.media_query, grid_column.get_bound(bp).max)
                if obj.glossary['media_queries'].get(bp.name) != media_query:
                    obj.glossary['media_queries'][bp.name] = media_query
                    sanitized = True
        except AttributeError:
            logger.warning("ImagePlugin(pk={}) has no ColumnPlugin as ancestor.".format(obj.pk))
            return
        return sanitized

plugin_pool.register_plugin(BootstrapImagePlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/jumbotron.py
================================================
import logging

from django.core.exceptions import ValidationError
from django.forms import widgets, BooleanField, ChoiceField
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

from entangled.forms import EntangledModelFormMixin

from cms.plugin_pool import plugin_pool
from cmsplugin_cascade import app_settings
from cmsplugin_cascade.fields import ColorField, MultiSizeField, CascadeImageField
from cmsplugin_cascade.image import ImagePropertyMixin
from cmsplugin_cascade.bootstrap4.plugin_base import BootstrapPluginBase
from cmsplugin_cascade.bootstrap4.container import ContainerGridMixin
from cmsplugin_cascade.bootstrap4.fields import BootstrapMultiSizeField
from cmsplugin_cascade.bootstrap4.picture import get_picture_elements

logger = logging.getLogger('cascade')


class ImageBackgroundMixin:
    @property
    def element_heights(self):
        element_heights = self.glossary.get('element_heights', {})
        for bp, media_query in self.glossary['media_queries'].items():
            if bp in element_heights:
                yield {'media': media_query['media'], 'height': element_heights[bp]}

    @property
    def background_color(self):
        try:
            color, disabled = self.glossary['background_color']
            if not disabled and disabled != 'disabled':
                return 'background-color: {};'.format(color)
        except (KeyError, TypeError, ValueError):
            pass
        return ''

    @property
    def background_attachment(self):
        try:
            return 'background-attachment: {background_attachment};'.format(**self.glossary)
        except KeyError:
            return ''

    @property
    def background_position(self):
        try:
            return 'background-position: {background_vertical_position} {background_horizontal_position};'.format(**self.glossary)
        except KeyError:
            return ''

    @property
    def background_repeat(self):
        try:
            return 'background-repeat: {background_repeat};'.format(**self.glossary)
        except KeyError:
            return ''

    @property
    def background_size(self):
        try:
            size = self.glossary['background_size']
            if size == 'width/height':
                size = self.glossary['background_width_height']
                return 'background-size: {width} {height};'.format(**size)
            else:
                return 'background-size: {};'.format(size)
        except KeyError:
            pass
        return ''


class JumbotronFormMixin(EntangledModelFormMixin):
    """
    Form class to validate the JumbotronPlugin.
    """
    ATTACHMENT_CHOICES = ['scroll', 'fixed', 'local']
    VERTICAL_POSITION_CHOICES = ['top', '10%', '20%', '30%', '40%', 'center', '60%', '70%', '80%', '90%', 'bottom']
    HORIZONTAL_POSITION_CHOICES = ['left', '10%', '20%', '30%', '40%', 'center', '60%', '70%', '80%', '90%', 'right']
    REPEAT_CHOICES = ['repeat', 'repeat-x', 'repeat-y', 'no-repeat']
    SIZE_CHOICES = ['auto', 'width/height', 'cover', 'contain']

    fluid = BooleanField(
        label=_("Is fluid"),
        initial=True,
        required=False,
        help_text=_("Shall this element occupy the entire horizontal space of its parent."),
    )

    element_heights = BootstrapMultiSizeField(
        label=("Element Heights"),
        required=True,
        allowed_units=['rem', 'px', 'auto'],
        initial='300px',
        help_text=_("This property specifies the height for each Bootstrap breakpoint."),
    )

    background_color = ColorField(
        label=_("Background color"),
    )

    image_file = CascadeImageField(
        label=_("Background image"),
        required=False,
    )

    background_repeat = ChoiceField(
        label=_("Background repeat"),
        choices=[(c, c) for c in REPEAT_CHOICES],
        widget=widgets.RadioSelect,
        initial='no-repeat',
        required=False,
        help_text=_("This property specifies how the background image repeates."),
    )

    background_attachment = ChoiceField(
        label=_("Background attachment"),
        choices=[(c, c) for c in ATTACHMENT_CHOICES],
        widget=widgets.RadioSelect,
        initial='local',
        required=False,
        help_text=_("This property specifies how to move the background image relative to the viewport."),
    )

    background_vertical_position = ChoiceField(
        label=_("Background vertical position"),
        choices=[(c, c) for c in VERTICAL_POSITION_CHOICES],
        initial='center',
        required=False,
        help_text=_("This property moves a background image vertically within its container."),
    )

    background_horizontal_position = ChoiceField(
        label=_("Background horizontal position"),
        choices=[(c, c) for c in HORIZONTAL_POSITION_CHOICES],
        initial='center',
        required=False,
        help_text=_("This property moves a background image horizontally within its container."),
    )

    background_size = ChoiceField(
        label=_("Background size"),
        choices=[(c, c) for c in SIZE_CHOICES],
        widget=widgets.RadioSelect,
        initial='auto',
        required=False,
        help_text=_("This property specifies how the background image is sized."),
    )

    background_width_height = MultiSizeField(
        ['width', 'height'],
        label=_("Background width/height"),
        sublabels=[_("Width"), _("Height")],
        allowed_units=['px', '%'],
        required=False,
        help_text=_("This property specifies the width and height of a background image in px or %."),
    )

    class Meta:
        entangled_fields = {'glossary': ['fluid', 'background_color', 'element_heights', 'image_file',
                                         'background_repeat', 'background_attachment',
                                         'background_vertical_position', 'background_horizontal_position',
                                         'background_size', 'background_width_height']}

    def validate_optional_field(self, name):
        field = self.fields[name]
        value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
        if value in field.empty_values:
            self.add_error(name, ValidationError(field.error_messages['required'], code='required'))
        else:
            return value

    def clean(self):
        cleaned_data = super().clean()
        if cleaned_data['image_file']:
            self.validate_optional_field('background_repeat')
            self.validate_optional_field('background_attachment')
            self.validate_optional_field('background_vertical_position')
            self.validate_optional_field('background_horizontal_position')
            if self.validate_optional_field('background_size') == 'width/height':
                try:
                    cleaned_data['background_width_height']['width']
                except KeyError:
                    msg = _("You must at least set a background width.")
                    self.add_error('background_width_height', msg)
                    raise ValidationError(msg)
        return cleaned_data


class BootstrapJumbotronPlugin(BootstrapPluginBase):
    name = _("Jumbotron")
    model_mixins = (ContainerGridMixin, ImagePropertyMixin, ImageBackgroundMixin)
    require_parent = False
    parent_classes = ['BootstrapContainerPlugin', 'BootstrapColumnPlugin']
    allow_children = True
    alien_child_classes = True
    form = JumbotronFormMixin
    raw_id_fields = ['image_file']
    render_template = 'cascade/bootstrap4/jumbotron.html'
    ring_plugin = 'JumbotronPlugin'
    footnote_html = """<p>
    For more information about the Jumbotron please read the
    <a href="https://getbootstrap.com/docs/4.3/components/jumbotron/" target="_new">Bootstrap documentation</a>.
    </p>"""

    class Media:
        js = ['admin/js/jquery.init.js', 'cascade/js/admin/jumbotronplugin.js']

    def render(self, context, instance, placeholder):
        # image shall be rendered in a responsive context using the ``<picture>`` element
        try:
            elements = get_picture_elements(instance)
        except Exception as exc:
            logger.warning("Unable generate picture elements. Reason: {}".format(exc))
        else:
            try:
                if instance.child_plugin_instances and instance.child_plugin_instances[0].plugin_type == 'BootstrapRowPlugin':
                    padding='padding: {0}px {0}px;'.format(int(app_settings.CMSPLUGIN_CASCADE['bootstrap4']['gutter']/2))
                    context.update({'add_gutter_if_child_is_BootstrapRowPlugin': padding,})
                context.update({
                    'elements': [e for e in elements if 'media' in e] if elements else [],
                    'CSS_PREFIXES': app_settings.CSS_PREFIXES,
                })
            except Exception as exc:
                logger.warning("Unable generate picture elements. Reason: {}".format(exc))
        return self.super(BootstrapJumbotronPlugin, self).render(context, instance, placeholder)

    @classmethod
    def sanitize_model(cls, obj):
        sanitized = False
        super().sanitize_model(obj)
        grid_container = obj.get_bound_plugin().get_grid_instance()
        obj.glossary.setdefault('media_queries', {})
        for bp, bound in grid_container.bounds.items():
            obj.glossary['media_queries'].setdefault(bp.name, {})
            width = round(bound.max)
            if obj.glossary['media_queries'][bp.name].get('width') != width:
                obj.glossary['media_queries'][bp.name]['width'] = width
                sanitized = True
            if obj.glossary['media_queries'][bp.name].get('media') != bp.media_query:
                obj.glossary['media_queries'][bp.name]['media'] = bp.media_query
                sanitized = True
        return sanitized

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = cls.super(BootstrapJumbotronPlugin, cls).get_css_classes(obj)
        if obj.glossary.get('fluid'):
            css_classes.append('jumbotron-fluid')
        else:
            css_classes.append('jumbotron')
        return css_classes

    @classmethod
    def get_identifier(cls, obj):
        identifier = super().get_identifier(obj)
        try:
            content = obj.image.name or obj.image.original_filename
        except AttributeError:
            content = _("Without background image")
        return format_html('{0}{1}', identifier, content)

plugin_pool.register_plugin(BootstrapJumbotronPlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/mixins.py
================================================
from django.forms.fields import ChoiceField
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from entangled.forms import EntangledModelFormMixin
from cmsplugin_cascade.utils import CascadeUtilitiesMixin
from cmsplugin_cascade.bootstrap4.grid import Breakpoint


class BootstrapUtilities:
    """
    Factory for building a class ``BootstrapUtilitiesMixin``. This class then is used as a mixin to
    all sorts of Bootstrap-4 plugins. Various Bootstrap-4 plugins are shipped using this mixin class
    in different configurations. These configurations can be overridden through the project's
    settings using:
    ```
    CMSPLUGIN_CASCADE['plugins_with_extra_mixins'] = {
        'Bootstrap<ANY>Plugin': BootstrapUtilities(
            BootstrapUtilities.background_and_color,
            BootstrapUtilities.margins,
            BootstrapUtilities.paddings,
            …
        ),
        …
    }
    ```

    The class ``BootstrapUtilities`` offers a bunch of property methods which return a list of
    input fields and/or select boxes. They then can be added to the plugin's editor. This is
    specially useful to add CSS classes from the utilities section of Bootstrap-4, such as
    margins, borders, colors, etc.
    """
    def __new__(cls, *args):
        form_fields = {}
        for arg in args:
            if isinstance(arg, property):
                form_fields.update(arg.fget(cls))

        class Meta:
            entangled_fields = {'glossary': list(form_fields.keys())}

        utility_form_mixin = type('UtilitiesFormMixin', (EntangledModelFormMixin,), dict(form_fields, Meta=Meta))
        return type('BootstrapUtilitiesMixin', (CascadeUtilitiesMixin,), {'utility_form_mixin': utility_form_mixin})

    @property
    def background_and_color(cls):
        choices = [
            ('', _("Default")),
            ('bg-primary text-white', _("Primary with white text")),
            ('bg-secondary text-white', _("Secondary with white text")),
            ('bg-success text-white', _("Success with white text")),
            ('bg-danger text-white', _("Danger with white text")),
            ('bg-warning text-white', _("Warning with white text")),
            ('bg-info text-white', _("Info with white text")),
            ('bg-light text-dark', _("Light with dark text")),
            ('bg-dark text-white', _("Dark with white text")),
            ('bg-white text-dark', _("White with dark text")),
            ('bg-transparent text-dark', _("Transparent with dark text")),
            ('bg-transparent text-white', _("Transparent with white text")),
        ]
        return {'background_and_color': ChoiceField(
            label=_("Background and color"),
            choices=choices,
            required=False,
            initial='',
        )}

    @property
    def margins(cls):
        form_fields = {}
        choices_format = [
            ('m-{}{}', _("4 sided margins ({})")),
            ('mx-{}{}', _("Horizontal margins ({})")),
            ('my-{}{}', _("Vertical margins ({})")),
            ('mt-{}{}', _("Top margin ({})")),
            ('mr-{}{}', _("Right margin ({})")),
            ('mb-{}{}', _("Bottom margin ({})")),
            ('ml-{}{}', _("Left margin ({})")),
        ]
        sizes = list(range(0, 6)) + ['auto']
        previous_label = ''
        for bp in Breakpoint:
            if bp == Breakpoint.xs:
                choices = [(c.format('', s), format_lazy(l, s)) for c, l in choices_format for s in sizes]
                choices.insert(0, ('', _("No Margins")))
            else:
                choices = [(c.format(bp.name + '-', s), format_lazy(l, s)) for c, l in choices_format for s in sizes]
                choices.insert(0, ('', format_lazy(_("Inherit margin from {}"), previous_label)))
            previous_label = bp.label
            form_fields['margins_{}'.format(bp.name)] = ChoiceField(
                label=format_lazy(_("Margins for {breakpoint}"), breakpoint=bp.label),
                choices=choices,
                required=False,
                initial='',
            )
        return form_fields

    @property
    def vertical_margins(cls):
        form_fields = {}
        choices_format = [
            ('my-{}{}', _("Vertical margins ({})")),
            ('mt-{}{}', _("Top margin ({})")),
            ('mb-{}{}', _("Bottom margin ({})")),
        ]
        sizes = list(range(0, 6)) + ['auto']
        previous_label = ''
        for bp in Breakpoint:
            if bp == Breakpoint.xs:
                choices = [(c.format('', s), format_lazy(l, s)) for c, l in choices_format for s in sizes]
                choices.insert(0, ('', _("No Margins")))
            else:
                choices = [(c.format(bp.name + '-', s), format_lazy(l, s)) for c, l in choices_format for s in sizes]
                choices.insert(0, ('', format_lazy(_("Inherit margin from {}"), previous_label)))
            previous_label = bp.label
            form_fields['margins_{}'.format(bp.name)] = ChoiceField(
                label=format_lazy(_("Margins for {breakpoint}"), breakpoint=bp.label),
                choices=choices,
                required=False,
                initial='',
            )
        return form_fields

    @property
    def paddings(cls):
        form_fields = {}
        choices_format = [
            ('p-{}{}', _("4 sided padding ({})")),
            ('px-{}{}', _("Horizontal padding ({})")),
            ('py-{}{}', _("Vertical padding ({})")),
            ('pt-{}{}', _("Top padding ({})")),
            ('pr-{}{}', _("Right padding ({})")),
            ('pb-{}{}', _("Bottom padding ({})")),
            ('pl-{}{}', _("Left padding ({})")),
        ]
        sizes = range(0, 6)
        previous_label = ''
        for bp in Breakpoint:
            if bp == Breakpoint.xs:
                choices = [(c.format('', s), format_lazy(l, s)) for c, l in choices_format for s in sizes]
                choices.insert(0, ('', _("No Padding")))
            else:
                choices = [(c.format(bp.name + '-', s), format_lazy(l, s)) for c, l in choices_format for s in sizes]
                choices.insert(0, ('', format_lazy(_("Inherit padding from {}"), previous_label)))
            previous_label = bp.label
            form_fields['padding_{}'.format(bp.name)] = ChoiceField(
                label=format_lazy(_("Padding for {breakpoint}"), breakpoint=bp.label),
                choices=choices,
                required=False,
                initial='',
            )
        return form_fields

    @property
    def floats(cls):
        form_fields = {}
        choices_format = [
            ('float-{}none', _("Do not float")),
            ('float-{}left', _("Float left")),
            ('float-{}right', _("Float right")),
        ]
        previous_label = ''
        for bp in Breakpoint:
            if bp == Breakpoint.xs:
                choices = [(c.format(''), l) for c, l in choices_format]
                choices.insert(0, ('', _("Unset")))
            else:
                choices = [(c.format(bp.name + '-'), l) for c, l in choices_format]
                choices.insert(0, ('', format_lazy(_("Inherit float from {}"), previous_label)))
            previous_label = bp.label
            form_fields['float_{}'.format(bp.name)] = ChoiceField(
                label=format_lazy(_("Floats for {breakpoint}"), breakpoint=bp.label),
                choices=choices,
                required=False,
                initial='',
            )
        return form_fields


================================================
FILE: cmsplugin_cascade/bootstrap4/picture.py
================================================
import logging
from django.forms import widgets, MultipleChoiceField
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _

from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.bootstrap4.grid import Breakpoint
from cmsplugin_cascade.bootstrap4.utils import get_picture_elements, IMAGE_RESIZE_OPTIONS, IMAGE_SHAPE_CHOICES
from cmsplugin_cascade.bootstrap4.fields import BootstrapMultiSizeField
from cmsplugin_cascade.image import ImageFormMixin, ImagePropertyMixin
from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin
from cmsplugin_cascade.link.plugin_base import LinkElementMixin

logger = logging.getLogger('cascade.bootstrap4')


class BootstrapPictureFormMixin(ImageFormMixin):
    responsive_heights = BootstrapMultiSizeField(
        label=_("Adapt Picture Heights"),
        required=False,
        require_all_fields=False,
        allowed_units=['px', '%'],
        initial='100%',
        help_text=_("Heights of picture in percent or pixels for distinct Bootstrap's breakpoints."),
    )

    responsive_zoom = BootstrapMultiSizeField(
        label=_("Adapt Picture Zoom"),
        required=False,
        require_all_fields=False,
        allowed_units=['%'],
        initial=['0%', '0%', '0%', '0%', '0%'],
        help_text=_("Magnification of picture in percent for distinct Bootstrap's breakpoints."),
    )

    resize_options = MultipleChoiceField(
        label=_("Resize Options"),
        choices=IMAGE_RESIZE_OPTIONS,
        widget=widgets.CheckboxSelectMultiple,
        initial=['subject_location', 'high_resolution'],
        help_text = _("Options to use when resizing the image."),
    )

    image_shapes = MultipleChoiceField(
        label=_("Image Shapes"),
        choices=IMAGE_SHAPE_CHOICES,
        widget=widgets.CheckboxSelectMultiple,
        initial=['img-fluid']
    )

    class Meta:
        entangled_fields = {'glossary': ['responsive_heights', 'responsive_zoom', 'resize_options', 'image_shapes']}


class BootstrapPicturePlugin(LinkPluginBase):
    name = _("Picture")
    module = 'Bootstrap'
    parent_classes = ['BootstrapColumnPlugin', 'SimpleWrapperPlugin']
    require_parent = True
    allow_children = False
    raw_id_fields = LinkPluginBase.raw_id_fields + ['image_file']
    model_mixins = (ImagePropertyMixin, LinkElementMixin,)
    admin_preview = False
    ring_plugin = 'PicturePlugin'
    form = type('BootstrapPictureForm', (LinkFormMixin, BootstrapPictureFormMixin), {'require_link': False})
    render_template = 'cascade/bootstrap4/linked-picture.html'
    default_css_class = 'img-fluid'
    default_css_attributes = ['image_shapes']
    html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'}
    html_tag_attributes.update(LinkPluginBase.html_tag_attributes)

    class Media:
        js = ['admin/js/jquery.init.js', 'cascade/js/admin/pictureplugin.js']

    def render(self, context, instance, placeholder):
        # image shall be rendered in a responsive context using the picture element
        context = self.super(BootstrapPicturePlugin, self).render(context, instance, placeholder)
        try:
            elements = get_picture_elements(instance)
        except Exception as exc:
            logger.warning("Unable generate picture elements. Reason: {}".format(exc))
        else:
            context.update({
                'instance': instance,
                'is_fluid': True,
                'placeholder': placeholder,
                'elements': elements,
            })
        return context

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = cls.super(BootstrapPicturePlugin, cls).get_css_classes(obj)
        css_class = obj.glossary.get('css_class')
        if css_class:
            css_classes.append(css_class)
        return css_classes

    @classmethod
    def get_identifier(cls, obj):
        try:
            content = str(obj.image)
        except AttributeError:
            content = _("No Picture")
        return mark_safe(content)

    @classmethod
    def sanitize_model(cls, obj):
        sanitized = False
        parent = obj.parent
        if parent:
            while parent.plugin_type != 'BootstrapColumnPlugin':
                parent = parent.parent
            grid_column = parent.get_bound_plugin().get_grid_instance()
            obj.glossary.setdefault('media_queries', {})
            for bp in Breakpoint:
                obj.glossary['media_queries'].setdefault(bp.name, {})
                width = round(grid_column.get_bound(bp).max)
                if obj.glossary['media_queries'][bp.name].get('width') != width:
                    obj.glossary['media_queries'][bp.name]['width'] = width
                    sanitized = True
                if obj.glossary['media_queries'][bp.name].get('media') != bp.media_query:
                    obj.glossary['media_queries'][bp.name]['media'] = bp.media_query
                    sanitized = True
        else:
            logger.warning("PicturePlugin(pk={}) has no ColumnPlugin as ancestor.".format(obj.pk))
            return 
        return sanitized

plugin_pool.register_plugin(BootstrapPicturePlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/plugin_base.py
================================================
import os
from django.template.loader import get_template, TemplateDoesNotExist
from cmsplugin_cascade import app_settings
from cmsplugin_cascade.plugin_base import CascadePluginBase


class BootstrapPluginBase(CascadePluginBase):
    module = 'Bootstrap'
    require_parent = True
    allow_children = True
    render_template = 'cascade/generic/wrapper.html'

    def get_render_template(self, context, instance, placeholder):
        render_template = getattr(self, 'render_template', None)
        if render_template and '{}' in render_template:
            try:
                # check if overridden template exists
                template = render_template.format(app_settings.CMSPLUGIN_CASCADE['bootstrap4']['template_basedir'])
                template = os.path.normpath(template)
                get_template(template)
                return template
            except (KeyError, TemplateDoesNotExist):
                template = render_template.format('')
                return os.path.normpath(template)
        return render_template


================================================
FILE: cmsplugin_cascade/bootstrap4/secondary_menu.py
================================================
from django.forms.fields import ChoiceField, IntegerField
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from entangled.forms import EntangledModelFormMixin
from cms.plugin_pool import plugin_pool
from cms.models.pagemodel import Page
from .plugin_base import BootstrapPluginBase


class SecondaryMenuFormMixin(EntangledModelFormMixin):
    page_id = ChoiceField(
        label=_("CMS Page Id"),
        help_text = _("Select a CMS page with a given unique Id (in advanced settings)."),
    )

    offset = IntegerField(
        label=_("Offset"),
        initial=0,
        help_text=_("Starting from which child menu."),
    )

    limit = IntegerField(
        label=_("Limit"),
        initial=100,
        help_text=_("Number of child menus."),
    )

    class Meta:
        entangled_fields = {'glossary': ['page_id', 'offset', 'limit']}

    def __init__(self, *args, **kwargs):
        choices = [(p.reverse_id, str(p)) for p in Page.objects.filter(reverse_id__isnull=False, publisher_is_draft=False)]
        self.base_fields['page_id'].choices = choices
        super().__init__(*args, **kwargs)


class BootstrapSecondaryMenuPlugin(BootstrapPluginBase):
    """
    Use this plugin to display a secondary menu in arbitrary locations.
    This renders links onto  all CMS pages, which are children of the selected Page Id.
    """
    name = _("Secondary Menu")
    default_css_class = 'list-group'
    require_parent = False
    parent_classes = None
    allow_children = False
    form = SecondaryMenuFormMixin
    render_template = 'cascade/bootstrap4/secmenu-list-group.html'

    @classmethod
    def get_identifier(cls, obj):
        return mark_safe(obj.glossary.get('page_id', ''))

    def render(self, context, instance, placeholder):
        context = self.super(BootstrapSecondaryMenuPlugin, self).render(context, instance, placeholder)
        context.update({
            'page_id': instance.glossary['page_id'],
            'offset': instance.glossary.get('offset', 0),
            'limit': instance.glossary.get('limit', 100),
        })
        return context

    @classmethod
    def sanitize_model(cls, instance):
        try:
            if int(instance.glossary['offset']) < 0 or int(instance.glossary['limit']) < 0:
                raise ValueError()
        except (KeyError, ValueError):
            instance.glossary['offset'] = 0
            instance.glossary['limit'] = 100

plugin_pool.register_plugin(BootstrapSecondaryMenuPlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/settings.py
================================================
from collections import OrderedDict
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from cmsplugin_cascade.extra_fields.config import PluginExtraFieldsConfig
from cmsplugin_cascade.bootstrap4.mixins import BootstrapUtilities
from .grid import Breakpoint, Bound


CASCADE_PLUGINS = ['accordion', 'buttons', 'card', 'carousel', 'container', 'embeds', 'icon', 'image', 'jumbotron',
                   'picture', 'tabs']
if 'cms_bootstrap' in settings.INSTALLED_APPS:
    CASCADE_PLUGINS.append('secondary_menu')


def set_defaults(config):
    config.setdefault('bootstrap4', {})
    config['bootstrap4'].setdefault('default_bounds', OrderedDict([
        (Breakpoint.xs, Bound(320, 572)),
        (Breakpoint.sm, Bound(540, 540)),
        (Breakpoint.md, Bound(720, 720)),
        (Breakpoint.lg, Bound(960, 960)),
        (Breakpoint.xl, Bound(1140, 1140)),
    ]))
    config['bootstrap4'].setdefault('fluid_bounds', OrderedDict([
        (Breakpoint.xs, Bound(320, 576)),
        (Breakpoint.sm, Bound(576, 768)),
        (Breakpoint.md, Bound(768, 992)),
        (Breakpoint.lg, Bound(992, 1200)),
        (Breakpoint.xl, Bound(1200, 1980)),
    ]))
    config['bootstrap4'].setdefault('gutter', 30)

    config['plugins_with_extra_mixins'].setdefault('BootstrapAccordionPlugin', BootstrapUtilities(
        BootstrapUtilities.margins,
    ))
    config['plugins_with_extra_mixins'].setdefault('BootstrapAccordionGroupPlugin', BootstrapUtilities(
        BootstrapUtilities.background_and_color,
        BootstrapUtilities.margins,
    ))
    config['plugins_with_extra_mixins'].setdefault('BootstrapCardPlugin', BootstrapUtilities(
        BootstrapUtilities.background_and_color,
        BootstrapUtilities.margins,
    ))
    config['plugins_with_extra_mixins'].setdefault('BootstrapCarouselPlugin', BootstrapUtilities(
        BootstrapUtilities.margins,
    ))
    config['plugins_with_extra_mixins'].setdefault('BootstrapContainerPlugin', BootstrapUtilities(
        BootstrapUtilities.paddings,
    ))
    config['plugins_with_extra_mixins'].setdefault('HeadingPlugin', BootstrapUtilities(
        BootstrapUtilities.margins,
    ))
    config['plugins_with_extra_mixins'].setdefault('HorizontalRulePlugin', BootstrapUtilities(
        BootstrapUtilities.margins,
    ))

    config['plugins_with_extra_fields'].setdefault('BootstrapTabSetPlugin', PluginExtraFieldsConfig(
        css_classes={
            'multiple': True,
            'class_names': ['nav-tabs', 'nav-pills', 'nav-fill', 'nav-justified'],
        },
    ))

    config['plugins_with_extra_render_templates'].setdefault('BootstrapSecondaryMenuPlugin', [
        ('cascade/bootstrap4/secmenu-list-group.html', _("List Group")),
        ('cascade/bootstrap4/secmenu-unstyled-list.html', _("Unstyled List"))
    ])


================================================
FILE: cmsplugin_cascade/bootstrap4/tabs.py
================================================
from django.forms import widgets
from django.forms.fields import BooleanField, CharField
from django.utils.translation import ngettext_lazy, gettext_lazy as _
from django.utils.text import Truncator
from django.utils.safestring import mark_safe
from django.forms.fields import IntegerField

from entangled.forms import EntangledModelFormMixin
from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.forms import ManageChildrenFormMixin
from cmsplugin_cascade.plugin_base import TransparentWrapper, TransparentContainer
from cmsplugin_cascade.widgets import NumberInputWidget
from .plugin_base import BootstrapPluginBase


class TabSetFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin):
    num_children = IntegerField(
        min_value=1,
        initial=1,
        widget=NumberInputWidget(attrs={'size': '3', 'style': 'width: 5em !important;'}),
        label=_("Number of Tabs"),
        help_text=_("Number can be adjusted at any time."),
    )

    justified = BooleanField(
        label=_("Justified tabs"),
        required=False,
    )

    class Meta:
        untangled_fields = ['num_children']
        entangled_fields = {'glossary': ['justified']}


class BootstrapTabSetPlugin(TransparentWrapper, BootstrapPluginBase):
    name = _("Tab Set")
    parent_classes = ['BootstrapColumnPlugin']
    direct_child_classes = ['BootstrapTabPanePlugin']
    require_parent = True
    allow_children = True
    form = TabSetFormMixin
    render_template = 'cascade/bootstrap4/{}tabset.html'
    default_css_class = 'nav-tabs'

    @classmethod
    def get_identifier(cls, instance):
        num_cols = instance.get_num_children()
        content = ngettext_lazy('with {} tab', 'with {} tabs', num_cols).format(num_cols)
        return mark_safe(content)

    @classmethod
    def get_css_classes(cls, obj):
        css_classes = super().get_css_classes(obj)
        if obj.glossary.get('justified'):
            css_classes.append('nav-fill')
        return css_classes

    def save_model(self, request, obj, form, change):
        wanted_children = int(form.cleaned_data.get('num_children'))
        super().save_model(request, obj, form, change)
        self.extend_children(obj, wanted_children, BootstrapTabPanePlugin)

plugin_pool.register_plugin(BootstrapTabSetPlugin)


class TabPaneFormMixin(EntangledModelFormMixin):
    tab_title = CharField(
        label=_("Tab Title"),
        widget=widgets.TextInput(attrs={'size': 80}),
    )

    class Meta:
        entangled_fields = {'glossary': ['tab_title']}


class BootstrapTabPanePlugin(TransparentContainer, BootstrapPluginBase):
    name = _("Tab Pane")
    direct_parent_classes = parent_classes = ['BootstrapTabSetPlugin']
    require_parent = True
    allow_children = True
    alien_child_classes = True
    form = TabPaneFormMixin

    @classmethod
    def get_identifier(cls, obj):
        content = obj.glossary.get('tab_title', '')
        if content:
            content = Truncator(content).words(3, truncate=' ...')
        return mark_safe(content)

plugin_pool.register_plugin(BootstrapTabPanePlugin)


================================================
FILE: cmsplugin_cascade/bootstrap4/utils.py
================================================
import logging
from django.utils.translation import gettext_lazy as _

from cmsplugin_cascade import app_settings
from cmsplugin_cascade.utils import (compute_aspect_ratio, get_image_size, parse_responsive_length,
   compute_aspect_ratio_with_glossary)

logger = logging.getLogger('cascade')

IMAGE_RESIZE_OPTIONS = [
    ('upscale', _("Upscale image")),
    ('crop', _("Crop image")),
    ('subject_location', _("With subject location")),
    ('high_resolution', _("Optimized for Retina")),
]

IMAGE_SHAPE_CHOICES = [
    ('img-fluid', _("Responsive")),
    ('rounded', _('Rounded')),
    ('rounded-circle', _('Circle')),
    ('img-thumbnail', _('Thumbnail')),
]


def get_image_tags(instance):
    """
    Create a context returning the tags to render an ``<img ...>`` element with
    ``sizes``, ``srcset``, a fallback ``src`` and if required inline styles.
    """
    if hasattr(instance, 'image') and hasattr(instance.image, 'exif'):
        aspect_ratio = compute_aspect_ratio(instance.image)
    elif 'image' in instance.glossary and 'width' in instance.glossary['image']:
        aspect_ratio = compute_aspect_ratio_with_glossary(instance.glossary)
    else:
        # if accessing the image file fails or fake image fails, abort here
        raise FileNotFoundError("Unable to compute aspect ratio of image")

    is_responsive = 'img-fluid' in instance.glossary.get('image_shapes', [])
    resize_options = instance.glossary.get('resize_options', {})
    crop = 'crop' in resize_options
    upscale = 'upscale' in resize_options
    if 'subject_location' in resize_options and hasattr(instance.image, 'subject_location'):
        subject_location = instance.image.subject_location
    else:
        subject_location = None
    tags = {'sizes': [], 'srcsets': {}, 'is_responsive': is_responsive, 'extra_styles': {}}
    if is_responsive:
        image_width = parse_responsive_length(instance.glossary.get('image_width_responsive') or '100%')
        assert(image_width[1]), "The given image has no valid width"
        if image_width[1] != 1.0:
            tags['extra_styles'].update({'max-width': '{:.0f}%'.format(100 * image_width[1])})
    else:
        image_width = parse_responsive_length(instance.glossary['image_width_fixed'])
        if not image_width[0]:
            image_width = (instance.image.width, image_width[1])
    try:
        image_height = parse_responsive_length(instance.glossary['image_height'])
    except KeyError:
        image_height = (None, None)
    if is_responsive:
        column_bounds_min = instance.glossary['column_bounds']['min']
        if 'high_resolution' in resize_options:
            column_bounds_max = 2 * instance.glossary['column_bounds']['max']
        else:
            column_bounds_max = instance.glossary['column_bounds']['max']
        num_steps = min(int((column_bounds_max - column_bounds_min) / app_settings.RESPONSIVE_IMAGE_STEP_SIZE),
                        app_settings.RESPONSIVE_IMAGE_MAX_STEPS)
        step_width, max_width = (column_bounds_max - column_bounds_min) / num_steps, 0
        for step in range(0, num_steps + 1):
            width = round(column_bounds_min + step_width * step)
            max_width = max(max_width, width)
            size = get_image_size(width, image_height, aspect_ratio)
            key = '{0}w'.format(*size)
            tags['srcsets'][key] = {'size': size, 'crop': crop, 'upscale': upscale,
                                    'subject_location': subject_location}
        tags['sizes'] = instance.glossary['media_queries'].values()
        # use an existing image as fallback for the <img ...> element
        if not max_width > 0:
            logger.warning('image tags: image max width is zero')
        size = (int(round(max_width)), int(round(max_width * aspect_ratio)))
    else:
        size = get_image_size(image_width[0], image_height, aspect_ratio)
        if 'high_resolution' in resize_options:
            tags['srcsets']['1x'] = {'size': size, 'crop': crop, 'upscale': upscale,
                                     'subject_location': subject_location}
            tags['srcsets']['2x'] = dict(tags['srcsets']['1x'], size=(size[0] * 2, size[1] * 2))
    tags['src'] = {'size': size, 'crop': crop, 'upscale': upscale, 'subject_location': subject_location}
    return tags


def get_picture_elements(instance):
    """
    Create a context, used to render a <picture> together with all its ``<source>`` elements:
    It returns a list of HTML elements, each containing the information to render a ``<source>``
    element.
    The purpose of this HTML entity is to display images with art directions. For normal images use
    the ``<img>`` element.
    """

    if hasattr(instance, 'image') and hasattr(instance.image, 'exif'):
        aspect_ratio = compute_aspect_ratio(instance.image)
    elif 'image' in instance.glossary and 'width' in instance.glossary['image']:
        aspect_ratio = compute_aspect_ratio_with_glossary(instance.glossary)
    else:
        # if accessing the image file fails or fake image fails, abort here
        logger.warning("Unable to compute aspect ratio of image '{}'".format(instance.image))
        return

    # container_max_heights = instance.glossary.get('container_max_heights', {})
    resize_options = instance.glossary.get('resize_options', {})
    crop = 'crop' in resize_options
    upscale = 'upscale' in resize_options
    if 'subject_location' in resize_options and hasattr(instance.image, 'subject_location'):
        subject_location = instance.image.subject_location
    else:
        subject_location = None
    max_width = 0
    max_zoom = 0
    elements = []
    for bp, media_query in instance.glossary['media_queries'].items():
        width, media = media_query['width'], media_query['media']
        max_width = max(max_width, width)
        size = None
        try:
            image_height = parse_responsive_length(instance.glossary['responsive_heights'][bp])
        except KeyError:
            image_height = (None, None)
        if image_height[0]:  # height was given in px
            size = (int(width), image_height[0])
        elif image_height[1]:  # height was given in %
            size = (int(width), int(round(width * aspect_ratio * image_height[1])))
        try:
            zoom = int(
                instance.glossary['responsive_zoom'][bp].strip().rstrip('%')
            )
        except (AttributeError, KeyError, ValueError):
            zoom = 0
        max_zoom = max(max_zoom, zoom)
        if size is None:
            # as fallback, adopt height to current width
            size = (int(width), int(round(width * aspect_ratio)))
        elem = {'tag': 'source', 'size': size, 'zoom': zoom, 'crop': crop,
                'upscale': upscale, 'subject_location': subject_location, 'media': media}
        if 'high_resolution' in resize_options:
            elem['size2'] = (size[0] * 2, size[1] * 2)
        elements.append(elem)

    # add a fallback image for old browsers which can't handle the <source> tags inside a <picture> element
    if image_height[1]:
        size = (int(max_width), int(round(max_width * aspect_ratio * image_height[1])))
    else:
        size = (int(max_width), int(round(max_width * aspect_ratio)))
    elements.append({'tag': 'img', 'size': size, 'zoom': max_zoom, 'crop': crop,
                     'upscale': upscale, 'subject_location': subject_location})
    return elements


================================================
FILE: cmsplugin_cascade/clipboard/__init__.py
================================================


================================================
FILE: cmsplugin_cascade/clipboard/admin.py
================================================
from django.contrib import admin
from django.contrib import messages
from django.forms import widgets
from django.forms.utils import flatatt
from django.db.models import JSONField
from django.templatetags.static import static
from django.utils.html import format_html
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

from cms.models.placeholderpluginmodel import PlaceholderReference
from cmsplugin_cascade.clipboard.utils import deserialize_to_clipboard, serialize_from_placeholder
from cmsplugin_cascade.models import CascadeClipboard


class JSONAdminWidget(widgets.Textarea):
    def __init__(self):
        attrs = {'cols': '40', 'rows': '3'}
        super(JSONAdminWidget, self).__init__(attrs)

    def render(self, name, value, attrs=None, renderer=None):
        if value is None:
            value = ''
        final_attrs = self.build_attrs(self.attrs, extra_attrs=dict(attrs, name=name))
        id_data = attrs.get('id', 'id_data')
        clippy_url = static('cascade/admin/clippy.svg')
        return format_html('<textarea{0}>\r\n{1}</textarea> '
            '<button data-clipboard-target="#{2}" type="button" title="{4}" class="clip-btn">'
                '<img src="{3}" alt="{4}">'
            '</button>\n'
            '<div class="status-line"><label></label><strong id="pasted_success">{5}</strong>'
            '<strong id="copied_success">{6}</strong></div>',
            flatatt(final_attrs), str(value), id_data, clippy_url,
            _("Copy to Clipboard"),
            _("Successfully pasted JSON data"),
            _("Successfully copied JSON data"))


@admin.register(CascadeClipboard)
class CascadeClipboardAdmin(admin.ModelAdmin):
    fields = ['identifier', ('created_by', 'created_at', 'last_accessed_at'), 'save_clipboard', 'restore_clipboard', 'data']
    readonly_fields = ['created_by', 'created_at', 'last_accessed_at', 'save_clipboard', 'restore_clipboard']
    formfield_overrides = {
        JSONField: {'widget': JSONAdminWidget},
    }
    list_display = ['identifier', 'created_by', 'created_at']

    class Media:
        css = {'all': ['cascade/css/admin/clipboard.css']}
        js = ['admin/js/jquery.init.js', 'cascade/js/admin/clipboard.js']

    def save_clipboard(self, obj):
        return format_html('<input type="submit" value="{}" class="default pull-left" name="save_clipboard" />',
                           _("Insert Data"))
    save_clipboard.short_description = _("From CMS Clipboard")

    def restore_clipboard(self, obj):
        return format_html('<input type="submit" value="{}" class="default pull-left" name="restore_clipboard" />',
                           _("Restore Data"))
    restore_clipboard.short_description = _("To CMS Clipboard")

    def save_model(self, request, obj, form, change):
        if request.POST.get('save_clipboard'):
            placeholder_reference = PlaceholderReference.objects.last()
            if placeholder_reference:
                placeholder = placeholder_reference.placeholder_ref
                obj.data = serialize_from_placeholder(placeholder, self.admin_site)
            request.POST = request.POST.copy()
            request.POST['_continue'] = True
            messages.add_message(request, messages.INFO, _("The clipboard's content has been persisted for later."))
        if request.POST.get('restore_clipboard'):
            request.POST = request.POST.copy()
            request.POST['_continue'] = True
            messages.add_message(request, messages.INFO, _("Persisted content has been restored to the clipboard."))
        if request.POST.get('restore_clipboard'):
            deserialize_to_clipboard(request, obj.data)
            obj.last_accessed_at = now()
        super().save_model(request, obj, form, change)



================================================
FILE: cmsplugin_cascade/clipboard/cms_plugins.py
================================================
import json

from django.contrib.admin import site as default_admin_site
from django.contrib.admin.helpers import AdminForm
from django.core.exceptions import PermissionDenied
from django.forms import CharField, ModelChoiceField
from django.shortcuts import render
from django.template.response import TemplateResponse
from django.urls import re_path, reverse
from django.utils.http import urlencode
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, get_language_from_request

from cms.plugin_base import CMSPluginBase, PluginMenuItem
from cms.plugin_pool import plugin_pool
from cms.toolbar.utils import get_plugin_tree_as_json
from cmsplugin_cascade.clipboard.forms import ClipboardBaseForm
from cmsplugin_cascade.clipboard.utils import deserialize_to_clipboard, serialize_from_placeholder
from cmsplugin_cascade.models import CascadeClipboard


class CascadeClipboardPlugin(CMSPluginBase):
    render_plugin = False
    change_form_template = 'admin/cms/page/plugin/change_form.html'

    def get_plugin_urls(self):
        urlpatterns = [
            re_path(r'^export-plugins/$', self.export_plugins_view, name='export_clipboard_plugins'),
            re_path(r'^import-plugins/$', self.import_plugins_view, name='import_clipboard_plugins'),
        ]
        return urlpatterns

    @classmethod
    def get_extra_placeholder_menu_items(cls, request, placeholder):
        data = urlencode({
            'placeholder': placeholder.pk,
            'language': get_language_from_request(request, check_path=True),
        })
        return [
            PluginMenuItem(
                _("Export to Clipboard"),
                reverse('admin:export_clipboard_plugins') + '?' + data,
                data={},
                action='modal',
                attributes={
                    'icon': 'export',
                },
            ),
            PluginMenuItem(
                _("Import from Clipboard"),
                reverse('admin:import_clipboard_plugins') + '?' + data,
                data={},
                action='modal',
                attributes={
                    'icon': 'import',
                },
            )
        ]

    def render_modal_window(self, request, form):
        """
        Render a modal popup window with a select box to edit the form
        """
        opts = self.model._meta
        fieldsets = [(None, {'fields': list(form.fields)})]
        adminForm = AdminForm(form, fieldsets, {}, [])
        context = {
            **default_admin_site.each_context(request),
            'title': form.title,
            'adminform': adminForm,
            'add': False,
            'change': True,
            'save_as': False,
            'has_add_permission': False,
            'has_change_permission': True,
            'opts': opts,
            'root_path': reverse('admin:index'),
            'is_popup': True,
            'app_label': opts.app_label,
            'media': self.media + form.media,
        }
        return TemplateResponse(request, self.change_form_template, context)

    def import_plugins_view(self, request):
        # TODO: check for permissions

        title = _("Import from Clipboard")
        if request.method == 'GET':
            Form = type('ClipboardImportForm', (ClipboardBaseForm,), {
                'clipboard': ModelChoiceField(
                    queryset=CascadeClipboard.objects.all(),
                    label=_("Select Clipboard Content"),
                    required=False,
                ),
                'title': title,
            })
            form = Form(request.GET)
            assert form.is_valid()
        elif request.method == 'POST':
            Form = type('ClipboardImportForm', (ClipboardBaseForm,), {
                'clipboard': ModelChoiceField(
                    queryset=CascadeClipboard.objects.all(),
                    label=_("Select Clipboard Content"),
                ),
                'title': title,
            })
            form = Form(request.POST)
            if form.is_valid():
                return self.paste_from_clipboard(request, form)
        return self.render_modal_window(request, form)

    def paste_from_clipboard(self, request, form):
        placeholder = form.cleaned_data['placeholder']
        language = form.cleaned_data['language']
        cascade_clipboard = form.cleaned_data['clipboard']
        tree_order = placeholder.get_plugin_tree_order(language)
        deserialize_to_clipboard(request, cascade_clipboard.data)
        cascade_clipboard.last_accessed_at = now()
        cascade_clipboard.save(update_fields=['last_accessed_at'])

        # detach plugins from clipboard and reattach them to current placeholder
        cb_placeholder_plugin = request.toolbar.clipboard.cmsplugin_set.filter(plugin_type='PlaceholderPlugin').first()
        cb_placeholder_instance, _ = cb_placeholder_plugin.get_plugin_instance()
        new_plugins = cb_placeholder_instance.placeholder_ref.get_plugins()
        new_plugins.update(placeholder=placeholder)

        # reorder root plugins in placeholder
        root_plugins = placeholder.get_plugins(language).filter(parent__isnull=True).order_by('changed_date')
        for position, plugin in enumerate(root_plugins.iterator()):
            plugin.update(position=position)
        placeholder.mark_as_dirty(language, clear_cache=False)

        # create a list of pasted plugins to be added to the structure view
        all_plugins = placeholder.get_plugins(language)
        if all_plugins.exists():
            new_plugins = placeholder.get_plugins(language).exclude(pk__in=tree_order)
            data = json.loads(get_plugin_tree_as_json(request, list(new_plugins)))
            data['plugin_order'] = tree_order + ['__COPY__']
        else:
            return render(request, 'cascade/admin/clipboard_reload_page.html')
        data['target_placeholder_id'] = placeholder.pk
        context = {'structure_data': json.dumps(data)}
        return render(request, 'cascade/admin/clipboard_paste_plugins.html', context)

    def export_plugins_view(self, request):
        if not request.user.is_staff:
            raise PermissionDenied

        title = _("Export to Clipboard")
        if request.method == 'GET':
            Form = type('ClipboardExportForm', (ClipboardBaseForm,), {
                'identifier': CharField(required=False),
                'title': title,
            })
            form = Form(request.GET)
            assert form.is_valid()
        elif request.method == 'POST':
            Form = type('ClipboardExportForm', (ClipboardBaseForm,), {
                'identifier': CharField(),
                'title': title,
            })
            form = Form(request.POST)
            if form.is_valid():
                return self.add_to_clipboard(request, form)
        return self.render_modal_window(request, form)

    def add_to_clipboard(self, request, form):
        placeholder = form.cleaned_data['placeholder']
        language = form.cleaned_data['language']
        identifier = form.cleaned_data['identifier']
        data = serialize_from_placeholder(placeholder)
        CascadeClipboard.objects.create(
            identifier=identifier,
            data=data,
            created_by=request.user,
        )
        return render(request, 'cascade/admin/clipboard_close_frame.html', {})

plugin_pool.register_plugin(CascadeClipboardPlugin)


================================================
FILE: cmsplugin_cascade/clipboard/cms_toolbars.py
================================================
from cms.toolbar_base import CMSToolbar
from cms.toolbar_pool import toolbar_pool


@toolbar_pool.register
class CascadeClipboardToolbar(CMSToolbar):
    class Media:
        css = {
            'all': ['cascade/css/admin/clipboard.css']
        }


================================================
FILE: cmsplugin_cascade/clipboard/forms.py
================================================
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

from cms.models import Placeholder
from cmsplugin_cascade.models import CascadeClipboard


class ClipboardBaseForm(forms.Form):
    placeholder = forms.ModelChoiceField(
        queryset=Placeholder.objects.all(),
        required=True,
        widget=forms.HiddenInput(),
    )

    language = forms.ChoiceField(
        choices=settings.LANGUAGES,
        required=True,
        widget=forms.HiddenInput(),
    )

    def clean_identifier(self):
        identifier = self.cleaned_data['identifier']
        if CascadeClipboard.objects.filter(identifier=identifier).exists():
            msg = _("This identifier has already been used, please choose another one.")
            raise ValidationError(msg)
        return identifier


================================================
FILE: cmsplugin_cascade/clipboard/utils.py
================================================
from django.contrib.admin import site as default_admin_site
from django.contrib import messages
from django.utils.translation import get_language_from_request

from cms.api import add_plugin
from cms.models.placeholderpluginmodel import PlaceholderReference
from cms.plugin_pool import plugin_pool

from djangocms_text_ckeditor.models import Text
from djangocms_text_ckeditor.utils import plugin_tags_to_id_list, replace_plugin_tags

from cmsplugin_cascade.models import CascadeElement


def serialize_from_placeholder(placeholder, admin_site=default_admin_site):
    """
    Create a serialized representation of all the plugins belonging to the clipboard.
    """
    def populate_data(parent, data):
        for child in plugin_qs.filter(parent=parent).order_by('position'):
            instance, plugin = child.get_plugin_instance(admin_site)
            plugin_type = plugin.__class__.__name__
            try:
                entry = (plugin_type, plugin.get_data_representation(instance), [])
            except AttributeError:
                if isinstance(instance, Text):
                    entry = (plugin_type, {'body': instance.body, 'pk': instance.pk}, [])
                else:
                    continue
            data.append(entry)
            populate_data(child, entry[2])

    data = {'plugins': []}
    plugin_qs = placeholder.cmsplugin_set.all()
    populate_data(None, data['plugins'])
    return data


def deserialize_to_clipboard(request, data):
    """
    Restore clipboard's content by creating plugins from given data.
    """
    def plugins_from_data(placeholder, parent, data):
        for plugin_type, data, children_data in data:
            try:
                plugin_class = plugin_pool.get_plugin(plugin_type)
            except Exception:
                messages.add_message(request, messages.ERROR, "Unable create plugin of type: {}".format(plugin_type))
                continue
            kwargs = dict(data)
            inlines = kwargs.pop('inlines', [])
            shared_glossary = kwargs.pop('shared_glossary', None)
            try:
                instance = add_plugin(placeholder, plugin_class, language, target=parent, **kwargs)
            except Exception:
                messages.add_message(request, messages.ERROR, "Unable to create structure for plugin: {}".format(plugin_class.name))
                continue
            if isinstance(instance, CascadeElement):
                instance.plugin_class.add_inline_elements(instance, inlines)
                instance.plugin_class.add_shared_reference(instance, shared_glossary)

            # for some unknown reasons add_plugin sets instance.numchild to 0,
            # but fixing and save()-ing 'instance' executes some filters in an unwanted manner
            plugins_from_data(placeholder, instance, children_data)

            if isinstance(instance, Text):
                # we must convert the old plugin IDs into the new ones,
                # otherwise links are not displayed
                id_dict = dict(zip(
                    plugin_tags_to_id_list(instance.body),
                    (t[0] for t in instance.get_children().values_list('id'))
                ))
                instance.body = replace_plugin_tags(instance.body, id_dict)
                instance.save()

    language = get_language_from_request(request, check_path=True)

    clipboard = request.toolbar.clipboard
    ref_plugin = clipboard.cmsplugin_set.first()
    if ref_plugin is None:
        # the clipboard is empty
        root_plugin = add_plugin(clipboard, 'PlaceholderPlugin', language, name='clipboard')
    else:
        # remove old entries from the clipboard
        try:
            root_plugin = ref_plugin.cms_placeholderreference
        except PlaceholderReference.DoesNotExist:
            root_plugin = add_plugin(clipboard, 'PlaceholderPlugin', language, name='clipboard')
        else:
            inst, _ = ref_plugin.get_plugin_instance()
            inst.placeholder_ref.get_plugins().delete()
    plugins_from_data(root_plugin.placeholder_ref, None, data['plugins'])


================================================
FILE: cmsplugin_cascade/cms_plugins.py
================================================
import sys
from importlib import import_module
from django.core.exceptions import ImproperlyConfigured
from . import app_settings


for module in app_settings.CASCADE_PLUGINS:
    try:
        # if a module was specified, load all plugins in module settings
        module_settings = import_module('{}.settings'.format(module))
        module_plugins = getattr(module_settings, 'CASCADE_PLUGINS', [])
        for p in module_plugins:
            try:
                import_module('{}.{}'.format(module, p))
            except ImportError as err:
                traceback = sys.exc_info()[2]
                msg = "Plugin {} as specified in {}.settings.CASCADE_PLUGINS could not be loaded: {}"
                raise ImproperlyConfigured(msg.format(p, module, err.with_traceback(traceback)))
    except ImportError:
        try:
            # otherwise try with cms_plugins in the named module
            import_module('{}.cms_plugins'.format(module))
        except ImportError:
            # otherwise just use the named module as plugin
            import_module('{}'.format(module))


================================================
FILE: cmsplugin_cascade/cms_toolbars.py
================================================
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from cms.extensions.toolbar import ExtensionToolbar
from cms.toolbar_pool import toolbar_pool
from cms.toolbar.items import Break
from cms.cms_toolbars import PAGE_MENU_SECOND_BREAK
from cmsplugin_cascade.models import CascadePage


class CascadePageToolbar(ExtensionToolbar):
    model = CascadePage

    def populate(self):
        current_page_menu = self._setup_extension_toolbar()
        if current_page_menu:
            # retrieves the instance of the current extension (if any) and the toolbar item URL
            page_extension, url = self.get_page_extension_admin()
            if url:
                position = current_page_menu.find_first(Break, identifier=PAGE_MENU_SECOND_BREAK)
                disabled = not self.toolbar.edit_mode_active
                current_page_menu.add_modal_item(_("Extra Page Fields"), position=position, url=url, disabled=disabled)

if settings.CMSPLUGIN_CASCADE['register_page_editor']:
    toolbar_pool.register(CascadePageToolbar)


================================================
FILE: cmsplugin_cascade/extra_fields/__init__.py
================================================
# -*- coding: utf-8 -*-


================================================
FILE: cmsplugin_cascade/extra_fields/admin.py
================================================
import re

from django.conf import settings
from django.contrib import admin
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.forms.fields import BooleanField, CharField, ChoiceField, MultipleChoiceField
from django.forms.models import ModelForm
from django.http.response import HttpResponse
from django.template.loader import render_to_string
from django.urls import re_path
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _, gettext

from entangled.forms import EntangledModelForm

from cms.plugin_pool import plugin_pool
from cmsplugin_cascade import app_settings
from cmsplugin_cascade.fields import SizeField
from cmsplugin_cascade.models import PluginExtraFields, TextEditorConfigFields, IconFont
from cmsplugin_cascade.extra_fields.mixins import ExtraFieldsMixin


class PluginExtraFieldsForm(EntangledModelForm):
    validation_pattern = re.compile(r'^[A-Za-z0-9_-]+$')

    class_names = CharField(
        label=_("CSS class names"),
        required=False,
        widget=widgets.TextInput(attrs={'style': 'width: 50em;'}),
        help_text=_("Freely selectable CSS classnames for this Plugin, separated by commas."),
    )

    multiple = BooleanField(
        label=_("Allow multiple"),
        required=False,
        help_text=_("Allow to select multiple of the above CSS classes."),
    )

    class Meta:
        untangled_fields = ['plugin_type', 'site']
        entangled_fields = {
            'css_classes': ['class_names', 'multiple'],
            'inline_styles': [],
        }

    def clean_class_names(self):
        value = self.cleaned_data['class_names']
        for val in value.split(','):
            val = val.strip()
            if val and not self.validation_pattern.match(val):
                msg = _("CSS class '{}' contains invalid characters.")
                raise ValidationError(msg.format(val))
        return value


class PluginExtraFieldsAdmin(admin.ModelAdmin):
    list_display = ['name', 'module', 'site', 'allowed_classes_styles']
    DISTANCE_UNITS = [
        ('px,em,rem,%', _("px, em, rem and %")),
        ('px,em,%', _("px, em and %")),
        ('px,rem,em', _("px, rem and em")),
        ('px,em', _("px and em")),
        ('px,rem,%', _("px, rem and %")),
        ('px,%', _("px and %")),
        ('px,rem', _("px and rem")),
        ('px', _("px")),
        ('%,rem', _("% and rem")),
        ('%', _("%")),
        ('px,em,rem,%,auto', _("px, em, rem, % and auto")),
        ('px,em,%,auto', _("px, em, % and auto")),
        ('px,rem,em,auto', _("px, rem, em and auto")),
        ('px,em,auto', _("px, em and auto")),
        ('px,rem,%,auto', _("px, rem, % and auto")),
        ('px,%,,auto', _("px, % and auto")),
        ('px,rem,auto', _("px, rem and auto")),
        ('px,auto', _("px and auto")),
        ('%,rem,auto', _("%, rem and auto")),
        ('%,auto', _("% and auto")),
    ]

    class Media:
        css = {'all': ('cascade/css/admin/partialfields.css',)}

    @cached_property
    def plugins_for_site(self):
        def show_in_backend(plugin):
            try:
                config = app_settings.CMSPLUGIN_CASCADE['plugins_with_extra_fields'][plugin.__name__]
            except KeyError:
                return False
            else:
                assert issubclass(plugin, ExtraFieldsMixin), "Expected plugin to be of type `ExtraFieldsMixin`."
                return config.allow_override

        cascade_plugins = set([p for p in plugin_pool.get_all_plugins() if show_in_backend(p)])
        return [(p.__name__, '{}: {}'.format(p.module, str(p.name))) for p in cascade_plugins]

    def get_form(self, request, obj=None, **kwargs):
        form_fields = {
            'plugin_type': ChoiceField(choices=self.plugins_for_site),
        }
        for style, choices_tuples in app_settings.CMSPLUGIN_CASCADE['extra_inline_styles'].items():
            translated_style = gettext(style)
            form_fields['extra_fields:{0}'.format(style)] = MultipleChoiceField(
                label=_("Customized {0}:").format(translated_style),
                choices=[(c, c) for c in choices_tuples[0]],
                required=False,
                widget=widgets.CheckboxSelectMultiple,
                help_text=_("Allow these extra inlines styles for the given plugin type."),
            )
            if issubclass(choices_tuples[1], SizeField):
                form_fields['extra_units:{0}'.format(style)] = ChoiceField(
                    label=_("Units for {0}:").format(translated_style),
                    choices=self.DISTANCE_UNITS,
                    required=False,
                    help_text=_("Allow these size units for customized {0} fields.").format(translated_style),
                )
        inline_styles_fields = list(form_fields.keys())
        form = type('PluginExtraFieldsForm', (PluginExtraFieldsForm,), form_fields)
        form._meta.entangled_fields['inline_styles'] = inline_styles_fields
        kwargs.setdefault('form', form)
        return super().get_form(request, obj=None, **kwargs)

    def has_add_permission(self, request):
        """
        Only if at least one plugin uses the class ExtraFieldsMixin, allow to add an instance.
        """
        has_permission = super().has_add_permission(request)
        return has_permission and len(self.plugins_for_site) > 0

    def module(self, obj):
        return plugin_pool.get_plugin(obj.plugin_type).module
    module.short_description = _("Module")

    def allowed_classes_styles(self, obj):
        clsn = [cn for cn in obj.css_classes.get('class_names', '').split(',') if cn]
        sef = [len(group) for ef, group in obj.inline_styles.items() if ef.startswith('extra_fields:')]
        return "{} / {}".format(len(clsn), sum(sef))
    allowed_classes_styles.short_description = _("Allowed Classes and Styles")

admin.site.register(PluginExtraFields, PluginExtraFieldsAdmin)


class TextEditorConfigForm(ModelForm):
    validation_pattern = re.compile(r'^[A-Za-z0-9_-]+$')

    class Meta:
        fields = ['name', 'element_type', 'css_classes']

    def clean_css_classes(self):
        css_classes = []
        for val in self.cleaned_data['css_classes'].split(' '):
            if val:
                if self.validation_pattern.match(val.strip()):
                    css_classes.append(val)
                else:
                    raise ValidationError(_("'%s' is not a valid CSS class name.") % val)
        return ' '.join(css_classes)


class TextEditorConfigAdmin(admin.ModelAdmin):
    list_display = ['name', 'element_type']
    form = TextEditorConfigForm

    def get_urls(self):
        return [
            re_path(r'^wysiwig-config\.js$', self.render_texteditor_config,
                name='cascade_texteditor_config'),
        ] + super().get_urls()

    def render_texteditor_config(self, request):
        context = {
            'text_editor_configs': TextEditorConfigFields.objects.all(),
        }
        if 'cmsplugin_cascade.icon' in settings.INSTALLED_APPS:
            context['icon_fonts'] = IconFont.objects.all()
        javascript = render_to_string('cascade/admin/ckeditor.wysiwyg.txt', context)
        return HttpResponse(javascript, content_type='application/javascript')

admin.site.register(TextEditorConfigFields, TextEditorConfigAdmin)


================================================
FILE: cmsplugin_cascade/extra_fields/config.py
================================================

class PluginExtraFieldsConfig:
    """
    Each Cascade Plugin can be configured to accept extra fields, such as an ID tag, one or more
    CSS classes or inlines styles. It is possible to configure these fields globally using an
    instance of this class, or to configure them on a per site base using the appropriate
    admin's backend interface at:
    ```
    *Start › django CMS Cascade › Custom CSS classes and styles › PluginExtraFields*
    ```
    :param allow_id_tag:
        If ``True``, allows to set the ``id`` attribute in HTML elements.

    :param css_classes:
        A dictionary containing:
          ``class_names`` a comma separated string of allowed class names.
          ``multiple`` a Boolean indicating if more multiple classes are allowed concurrently.

    :param inline_styles:
        A dictionary containing:

    :param allow_override:
        If ``True``, allows to override this configuration using the admin's backend interface.
    """
    def __init__(self, allow_id_tag=False, css_classes=None, inline_styles=None, allow_override=True):
        self.allow_id_tag = allow_id_tag
        self.css_classes = dict(multiple='', class_names='') if css_classes is None else dict(css_classes)
        self.inline_styles = {} if inline_styles is None else dict(inline_styles)
        self.allow_override = allow_override


================================================
FILE: cmsplugin_cascade/extra_fields/mixins.py
================================================
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ObjectDoesNotExist
from django.forms import MediaDefiningClass, widgets
from django.forms.fields import CharField, ChoiceField, MultipleChoiceField
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from entangled.forms import EntangledModelFormMixin
from cmsplugin_cascade import app_settings
from cmsplugin_cascade.fields import SizeField


class ExtraFieldsMixin(metaclass=MediaDefiningClass):
    """
    If a Cascade plugin is listed in ``settings.CMSPLUGIN_CASCADE['plugins_with_extra_fields']``,
    then this ``ExtraFieldsMixin`` class is added automatically to its plugin class in order to
    offer extra fields for customizing CSS classes and styles.
    """

    def __str__(self):
        return self.plugin_class.get_identifier(self)

    def get_form(self, request, obj=None, **kwargs):
        from cmsplugin_cascade.models import PluginExtraFields
        from .config import PluginExtraFieldsConfig

        clsname = self.__class__.__name__
        try:
            site = get_current_site(request)
            extra_fields = PluginExtraFields.objects.get(plugin_type=clsname, site=site)
        except ObjectDoesNotExist:
            extra_fields = app_settings.CMSPLUGIN_CASCADE['plugins_with_extra_fields'].get(clsname)

        if isinstance(extra_fields, (PluginExtraFields, PluginExtraFieldsConfig)):
            form_fields = {}

            # add a text input field to let the user name an ID tag for this HTML element
            if extra_fields.allow_id_tag:
                form_fields['extra_element_id'] = CharField(
                    label=_("Named Element ID"),
                )

            # add a select box to let the user choose one or more CSS classes
            class_names, choices = extra_fields.css_classes.get('class_names'), None
            if isinstance(class_names, (list, tuple)):
                choices = [(clsname, clsname) for clsname in class_names]
            elif isinstance(class_names, str):
                choices = [(clsname, clsname) for clsname in class_names.replace(' ', ',').split(',') if clsname]
            if choices:
                if extra_fields.css_classes.get('multiple'):
                    form_fields['extra_css_classes'] = MultipleChoiceField(
                        label=_("Customized CSS Classes"),
                        choices=choices,
                        required=False,
                        widget=widgets.CheckboxSelectMultiple,
                        help_text=_("Customized CSS classes to be added to this element."),
                    )
                else:
                    choices.insert(0, (None, _("Select CSS")))
                    form_fields['extra_css_classes'] = ChoiceField(
                        label=_("Customized CSS Class"),
                        choices=choices,
                        required=False,
                        help_text=_("Customized CSS class to be added to this element."),
                    )

            # add input fields to let the user enter styling information
            for style, choices_list in app_settings.CMSPLUGIN_CASCADE['extra_inline_styles'].items():
                inline_styles = extra_fields.inline_styles.get('extra_fields:{0}'.format(style))
                if not inline_styles:
                    continue
                Field = choices_list[1]
                for inline_style in inline_styles:
                    key = 'extra_inline_styles:{0}'.format(inline_style)
                    field_kwargs = {
                        'label': '{0}: {1}'.format(style, inline_style),
                        'required': False,
                    }
                    if issubclass(Field, SizeField):
                        field_kwargs['allowed_units'] = extra_fields.inline_styles.get('extra_units:{0}'.format(style)).split(',')
                    form_fields[key] = Field(**field_kwargs)

            # extend the form with some extra fields
            base_form = kwargs.pop('form', self.form)
            assert issubclass(base_form, EntangledModelFormMixin), "Form must inherit from EntangledModelFormMixin"
            class Meta:
                entangled_fields = {'glossary': list(form_fields.keys())}
            form_fields['Meta'] = Meta
            kwargs['form'] = type(base_form.__name__, (base_form,), form_fields)
        return super().get_form(request, obj, **kwargs)

    @classmethod
    def get_css_classes(cls, obj):
        """Enrich list of CSS classes with customized ones"""
        css_classes = super().get_css_classes(obj)
        extra_css_classes = obj.glossary.get('extra_css_classes')
        if extra_css_classes:
            if isinstance(extra_css_classes, str):
                css_classes.append(extra_css_classes)
            elif isinstance(extra_css_classes, (list, tuple)):
                css_classes.extend(extra_css_classes)
        return css_classes

    @classmethod
    def get_inline_styles(cls, obj):
        """Enrich inline CSS styles with customized ones"""
        inline_styles = super().get_inline_styles(obj)
        extra_inline_styles = app_settings.CMSPLUGIN_CASCADE['extra_inline_styles']
        for key, eis in obj.glossary.items():
            if key.startswith('extra_inline_styles:'):
                _, prop = key.split(':')
                if isinstance(eis, dict):
                    # a multi value field, storing values as dict
                    inline_styles.update(dict((k, v) for k, v in eis.items() if v))
                elif isinstance(eis, (list, tuple)):
                    # a multi value field, storing values as list
                    for props, field_class in extra_inline_styles.values():
                        if prop in props:
                            inline_styles.update({prop: field_class.css_value(eis)})
                            break
                elif isinstance(eis, str):
                    inline_styles.update({prop: eis})
        return inline_styles

    @classmethod
    def get_html_tag_attributes(cls, obj):
        attributes = super().get_html_tag_attributes(obj)
        extra_element_id = obj.glossary.get('extra_element_id')
        if extra_element_id:
            attributes.update(id=extra_element_id)
        return attributes

    @classmethod
    def get_identifier(cls, obj):
        identifier = super().get_identifier(obj)
        extra_element_id = obj.glossary and obj.glossary.get('extra_element_id')
        if extra_element_id:
            return format_html('{0}<em>{1}:</em> ', identifier, extra_element_id)
        return identifier


================================================
FILE: cmsplugin_cascade/fields.py
================================================
import re
import json
import warnings

from django.apps import apps
from django.core.exceptions import ValidationError
from django.db.models.fields.related import ManyToOneRel
from django.forms import widgets
from django.forms.fields import Field, CharField, ChoiceField, BooleanField, MultiValueField
from django.forms.utils import ErrorList
from django.core.validators import ProhibitNullCharactersValidator
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _, pgettext

from cmsplugin_cascade import app_settings
from cmsplugin_cascade.widgets import ColorPickerWidget, BorderChoiceWidget, MultipleTextInputWidget
from filer.fields.image import FilerImageField, AdminImageFormField
from filer.settings import settings as filer_settings


class GlossaryField:
    """
    Deprecated.
    Behave similar to django.forms.Field, encapsulating a partial dictionary, stored as
    JSONField in the database.
    """
    def __init__(self, widget, label=None, name=None, initial='', help_text='', error_class=ErrorList):
        warnings.warn("GlossaryField is deprecated")
        self.name = name
        if not isinstance(widget, widgets.Widget):
            raise AttributeError('`widget` must inherit from django.forms.widgets.Widget')
        if label is None:
            label = name
        self.widget = widget
        self.label = label
        self.initial = initial
        self.help_text = help_text
        self.error_class = error_class

    def run_validators(self, value):
        if not callable(getattr(self.widget, 'validate', None)):
            return
        errors = []
        if callable(getattr(self.widget, '__iter__', None)):
            for field_name in self.widget:
                try:
                    self.widget.validate(value.get(self.name), field_name)
                except ValidationError as e:
                    if isinstance(getattr(e, 'params', None), dict):
                        e.params.update(label=self.label)
                    messages = self.error_class([m for m in e.messages])
                    errors.extend(messages)
        else:
            try:
                self.widget.validate(value.get(self.name))
            except ValidationError as e:
                if isinstance(getattr(e, 'params', None), dict):
                    e.params.update(label=self.label)
                errors = self.error_class([m for m in e.messages])
        if errors:
            raise ValidationError(errors)

    def get_element_ids(self, prefix_id):
        """
        Returns a single or a list of element ids, one for each input widget of this field
        """
        if isinstance(self.widget, widgets.MultiWidget):
            ids = ['{0}_{1}_{2}'.format(prefix_id, self.name, field_name) for field_name in self.widget]
        elif isinstance(self.widget, (widgets.SelectMultiple, widgets.RadioSelect)):
            ids = ['{0}_{1}_{2}'.format(prefix_id, self.name, k) for k in range(len(self.widget.choices))]
        else:
            ids = ['{0}_{1}'.format(prefix_id, self.name)]
        return ids


class BorderChoiceField(MultiValueField):
    BORDER_STYLES = ['none', 'solid', 'dashed', 'dotted', 'double', 'groove', 'hidden',
                     'inset', 'outset', 'ridge']

    def __init__(self, *args, **kwargs):
        choices = [(s, s) for s in self.BORDER_STYLES]
        with_alpha = kwargs.pop('with_alpha', app_settings.CMSPLUGIN_CASCADE['color_picker_with_alpha'])
        widget = kwargs.pop('widget', BorderChoiceWidget(choices, with_alpha))
        fields = [
            SizeField(),
            ChoiceField(choices=choices),
            CharField(),
        ]
        kwargs['initial'] = ['0px', 'none', '#000000']
        super().__init__(fields=fields, widget=widget, *args, **kwargs)

    def prepare_value(self, value):
        return value

    def compress(self, data_list):
        return data_list

    @classmethod
    def css_value(self, values):
        return ' '.join(values)


class SelectTextAlignField(ChoiceField):
    CHOICES = [('left', 'left'), ('center', 'center'), ('right', 'right'), ('justify', 'justify')]

    def __init__(self, *args, **kwargs):
        super().__init__(choices=self.CHOICES, *args, **kwargs)


class SelectOverflowField(ChoiceField):
    CHOICES = [('auto', 'auto'), ('scroll', 'scroll'), ('hidden', 'hidden')]

    def __init__(self, *args, **kwargs):
        super().__init__(choices=self.CHOICES, *args, **kwargs)


@deconstructible
class ColorValidator():
    message = _("'%(color)s' is not a valid color code.")
    code = 'invalid_color_code'

    def __init__(self, with_alpha):
        if with_alpha:
            self.validation_pattern = re.compile(r'(#(?:[0-9a-fA-F]{2}){2,4}|(#[0-9a-fA-F]{3})|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))')
        else:
            self.validation_pattern = re.compile(r'(#(?:[0-9a-fA-F]{2}){2,3}|(#[0-9a-fA-F]{3})|(rgb|hsl))')

    def __call__(self, value):
        color, inherit = value
        match = self.validation_pattern.match(color)
        if not (inherit or match):
            params = {'color': color}
            raise ValidationError(self.message, code=self.code, params=params)

    def __eq__(self, other):
        return (
            isinstance(other, self.__class__) and
            self.validation_pattern == other.validation_pattern and
            self.message == other.message and
            self.code == other.code
        )


class ColorField(MultiValueField):
    DEFAULT_COLOR = '#808080'

    def __init__(self, inherit_color=False, default_color=DEFAULT_COLOR, *args, **kwargs):
        kwargs.pop('required', None)
        with_alpha = kwargs.pop('with_alpha', app_settings.CMSPLUGIN_CASCADE['color_picker_with_alpha'])
        widget = kwargs.pop('widget', ColorPickerWidget(with_alpha))
        fields = [
            CharField(initial=default_color),
            BooleanField(initial=inherit_color, required=False),
        ]
        kwargs['initial'] = [default_color, inherit_color]
        super().__init__(fields=fields, widget=widget, *args, **kwargs)
        self.validators.append(ColorValidator(with_alpha))
        self.validators.append(ProhibitNullCharactersValidator())

    def compress(self, data_list):
        self.run_validators(data_list)
        return data_list

    @classmethod
    def css_value(self, values):
        return values[0]


@deconstructible
class SizeUnitValidator():
    allowed_units = []
    message = _("'%(value)s' is not a valid size unit. Allowed units are: %(allowed_units)s.")
    code = 'invalid_size_unit'

    def __init__(self, allowed_units=None, allow_negative=True):
        possible_units = ['rem', 'px', 'em', '%', 'auto']
        if allowed_units is None:
            self.allowed_units = possible_units
        else:
            self.allowed_units = [au for au in allowed_units if au in possible_units]
        units_with_value = list(self.allowed_units)
        if 'auto' in self.allowed_units:
            self.allow_auto = True
            units_with_value.remove('auto')
        else:
            self.allow_auto = False
        if allow_negative:
            patterns = r'^(-?\d+(\.\d+)?)({})$'.format('|'.join(units_with_value))
        else:
            patterns = r'^(\d+(\.\d+)?)({})$'.format('|'.join(units_with_value))
        self.validation_pattern = re.compile(patterns)

    def __call__(self, value):
        if self.allow_auto and value == 'auto':
            return
        match = self.validation_pattern.match(value)
        try:
            float(match.group(1))
        except (AttributeError, ValueError):
            allowed_units = " {} ".format(pgettext('allowed_units', "or")).join("'{}'".format(u) for u in self.allowed_units)
            params = {'value': value, 'allowed_units': allowed_units}
            raise ValidationError(self.message, code=self.code, params=params)

    def __eq__(self, other):
        return (
            isinstance(other, self.__class__) and
            self.allowed_units == other.allowed_units and
            self.message == other.message and
            self.code == other.code
        )


class SizeField(Field):
    """
    Use this field for validating input containing a value ending in ``px``, ``em``, ``rem`` or ``%``.
    Use it for values representing a size, margin, padding, width or height.
    """
    def __init__(self, *, allowed_units=None, **kwargs):
        self.empty_value = ''
        super().__init__(**kwargs)
        self.validators.append(SizeUnitValidator(allowed_units))
        self.validators.append(ProhibitNullCharactersValidator())

    def to_python(self, value):
        """Return a stripped string."""
        if value not in self.empty_values:
            value = str(value).strip()
        if value in self.empty_values:
            return self.empty_value
        return value


class MultiSizeField(MultiValueField):
    """
    Some size input fields must be specified per Bootstrap breakpoint. Use this multiple
    input field to handle this.
    """
    def __init__(self, properties, sublabels=None, *args, **kwargs):
        required = kwargs.pop('required', False)
        require_all_fields = kwargs.pop('require_all_fields', required)
        initial = kwargs.pop('initial', None)
        if isinstance(initial, (list, tuple)):
            if len(initial) != len(properties):
                raise ValueError("The number of initial values must be {}.".format(len(properties)))
            initial = dict(zip(properties, initial))
        elif not isinstance(initial, dict):
            initial = {prop: initial for prop in properties}
        allowed_units = kwargs.pop('allowed_units', None)
        fields = [SizeField(required=required, allowed_units=allowed_units)] * len(properties)
        if sublabels is None:
            sublabels = properties
        widget = MultipleTextInputWidget(sublabels)
        super().__init__(fields=fields, widget=widget, required=required,
                         require_all_fields=require_all_fields, initial=initial, *args, **kwargs)
        self.properties = list(properties)

    def prepare_value(self, value):
        """Transform dict from DB into list"""
        if isinstance(value, dict):
            return [value.get(prop) for prop in self.properties]
        return value

    def compress(self, data_list):
        """Transform list into dict for DB"""
        return {prop: value for prop, value in zip(self.properties, data_list)}


class HiddenDictField(Field):
    widget = widgets.HiddenInput

    def prepare_value(self, value):
        """Transform dict from DB into list"""
        if isinstance(value, dict):
            return json.dumps(value)
        return value

    def clean(self, value):
        try:
            return json.loads(value)
        except:
            raise ValidationError("Invalid Field Value")


class CascadeImageField(AdminImageFormField):
    def __init__(self, *args, **kwargs):
        model_name_tuple = filer_settings.FILER_IMAGE_MODEL.split('.')
        Image = apps.get_model(*model_name_tuple, require_ready=False)
        kwargs.setdefault('label', _("Image"))
        super().__init__(
            ManyToOneRel(FilerImageField, Image, 'file_ptr'),
            Image.objects.all(),
            'image_file', *args, **kwargs)


================================================
FILE: cmsplugin_cascade/forms.py
================================================
from django.forms.formsets import DELETION_FIELD_NAME
from django.forms.models import ModelForm

from entangled.forms import EntangledModelFormMixin, EntangledModelForm


class ManageChildrenFormMixin:
    """
    Classes derived from ``CascadePluginBase`` can optionally add this mixin class to their form,
    offering the input field ``num_children`` in their plugin editor. The content of this field is
    not persisted in the plugin's model.
    It allows the client to modify the number of children attached to this plugin.
    """
    class Meta:
        fields = ['num_children']

    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance')
        if instance:
            initial = {'num_children': instance.get_num_children()}
            kwargs.update(initial=initial)
        super().__init__(*args, **kwargs)


class CascadeModelFormMixin(EntangledModelFormMixin):
    """
    Classes inheriting from InlineAdmin and defining their own `form` shall use this special
    variant of an `EntangledModelForm`, otherwise deletion of inlined elements does not work.
    """
    def _clean_form(self):
        internal_fields = ['cascade_element', 'id', DELETION_FIELD_NAME]
        internal_cleaned_data = {key: val for key, val in self.cleaned_data.items() if key in internal_fields}
        super()._clean_form()
        self.cleaned_data.update(internal_cleaned_data)


class CascadeModelForm(CascadeModelFormMixin, ModelForm):
    """
    A convenience class to create a ModelForms to be used with djangocms-cascade
    """


================================================
FILE: cmsplugin_cascade/generic/__init__.py
================================================


================================================
FILE: cmsplugin_cascade/generic/custom_snippet.py
================================================
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from cms.plugin_pool import plugin_pool

from cmsplugin_cascade import app_settings
from cmsplugin_cascade.plugin_base import CascadePluginBase, TransparentContainer


class CustomSnippetPlugin(TransparentContainer, CascadePluginBase):
    """
    Allows to add a customized template anywhere. This plugins will be registered only if the
    project added a template using the configuration setting 'plugins_with_extra_render_templates'.
    """
    name = _("Custom Snippet")
    require_parent = False
    allow_children = True
    alien_child_classes = True
    render_template_choices = dict(app_settings.CMSPLUGIN_CASCADE['plugins_with_extra_render_templates'].get('CustomSnippetPlugin', ()))
    render_template = 'cascade/generic/does_not_exist.html'  # default in case the template could not be found

    @classmethod
    def get_identifier(cls, instance):
        render_template = instance.glossary.get('render_template')
        if render_template:
            return format_html('{}', cls.render_template_choices.get(render_template))


if CustomSnippetPlugin.render_template_choices:
    # register only, if at least one template has been defined
    plugin_pool.register_plugin(CustomSnippetPlugin)


================================================
FILE: cmsplugin_cascade/generic/heading.py
================================================
from django.forms import widgets, CharField, ChoiceField
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from cms.plugin_pool import plugin_pool
from entangled.forms import EntangledModelFormMixin

from cmsplugin_cascade.plugin_base import CascadePluginBase


class HeadingFormMixin(EntangledModelFormMixin):
    TAG_TYPES = [('h{}'.format(k), _("Heading {}").format(k)) for k in range(1, 7)]

    tag_type = ChoiceField(
        choices=TAG_TYPES,
        label=_("Structure Level"),
    )

    content = CharField(
        label=_("Content"),
        widget=widgets.TextInput(attrs={'style': 'width: calc(100% - 12em); padding-right: 0; font-weight: bold; font-size: 125%;'}),
    )

    class Meta:
        entangled_fields = {'glossary': ['tag_type', 'content']}


class HeadingPlugin(CascadePluginBase):
    name = _("Heading")
    parent_classes = None
    allow_children = False
    form = HeadingFormMixin
    render_template = 'cascade/generic/heading.html'

    @classmethod
    def get_identifier(cls, instance):
        tag_type = instance.glossary.get('tag_type')
        content = mark_safe(instance.glossary.get('content', ''))
        if tag_type:
            return format_html('<code>{0}</code>: {1}', tag_type, content)
        return content

    def render(self, context, instance, placeholder):
        context = self.super(HeadingPlugin, self).render(context, instance, placeholder)
        context.update({'content': mark_safe(instance.glossary.get('content', ''))})
        return context

plugin_pool.register_plugin(HeadingPlugin)


================================================
FILE: cmsplugin_cascade/generic/horizontal_rule.py
================================================
from django.utils.translation import gettext_lazy as _

from cms.plugin_pool import plugin_pool
from cmsplugin_cascade.plugin_base import CascadePluginBase


class HorizontalRulePlugin(CascadePluginBase):
    name = _("Horizontal Rule")
    parent_classes = None
    allow_children = False
    tag_type = 'hr'
    render_template = 'cascade/generic/single.html'

plugin_pool.register_plugin(HorizontalRulePlugin)


================================================
FILE: cmsplugin_cascade/generic/mixins.py
================================================
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.forms.fields import CharField
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

from entangled.forms import EntangledModelFormMixin
from cmsplugin_cascade import app_settings
from cmsplugin_cascade.models import CascadePage


class SectionFormMixin(EntangledModelFormMixin):
    element_id = CharField(
        label=_("Id"),
        max_length=15,
        required=False,
        help_text=_("A unique identifier for this element (please don't use any special characters, punctuations, etc.) May be used as anchor-link: #id.")
    )

    class Meta:
        entangled_fields = {'glossary': ['element_id']}

    def clean_element_id(self):
        element_id = self.cleaned_data['element_id']
        self.check_unique_element_id(self.instance, element_id)
        return element_id

    @classmethod
    def check_unique_element_id(cls, instance, element_id):
        """
        Check for uniqueness of the given element_id for the current page.
        Return None if instance is not yet associated with a page.
        """
        if not element_id:
            return
        try:
            element_ids = instance.placeholder.page.cascadepage.glossary['element_ids'][instance.language]
        except (AttributeError, KeyError, ObjectDoesNotExist):
            return
        else:
            for key, value in element_ids.items():
                if str(key) != str(instance.pk) and element_id == value:
                    msg = _("The element ID '{}' is not unique for this page.")
                    raise ValidationError(msg.format(element_id))


class SectionModelMixin:
    def element_id(self):
        id_attr = self.glossary.get('element_id')
        if id_attr:
            return '{bookmark_prefix}{0}'.format(id_attr, **app_settings.CMSPLUGIN_CASCADE)

    def delete(self, *args, **kwargs):
        try:
            self.placeholder.page.cascadepage.glossary['element_ids'][self.language].pop(str(self.pk))
        except (AttributeError, KeyError, ObjectDoesNotExist):
            pass
        else:
            self.placeholder.page.cascadepage.save()
        super().delete(*args, **kwargs)


class SectionMixin:
    def get_form(self, request, obj=None, **kwargs):
        form = kwargs.get('form', self.form)
        assert issubclass(form, EntangledModelFormMixin), "Form must inherit from EntangledModelFormMixin"
        kwargs['form'] = type(form.__name__, (SectionFormMixin, form), {})
        return super().get_form(request, obj, **kwargs)

    @classmethod
    def get_identifier(cls, instance):
        try:
            element_id = instance.glossary['element_id'][instance.language]
        except (KeyError, TypeError):
            pass
        else:
            if element_id:
                return format_html('<code>id="{0}"</code>', element_id)
        return super().get_identifier(instance)

    def save_model(self, request, obj, form, change):
        super().save_model(request, obj, form, change)
        element_id = obj.glossary['element_id']
        if not change:
            # when adding a new element, `element_id` can not be validated for uniqueness
            postfix = 0
            # check if form simplewrapper has function check_unique_element_id
            if not 'check_unique_element_id' in dir(form):
                form_ = SectionFormMixin
            else:
                form_ = form
            while True:
                try:
                    form_.check_unique_element_id(obj, element_id)
                except ValidationError:
                    postfix += 1
                    element_id = '{element_id}_{0}'.format(postfix, **obj.glossary)
                else:
                    break
            if postfix:
                obj.glossary['element_id'] = element_id
                obj.save()

        cms_page = obj.placeholder.page
        if cms_page:
            # storing the element_id on a placholder only makes sense, if it is non-static
            CascadePage.assure_relation(cms_page)
            cms_page.cascadepage.glossary.setdefault('element_ids', {})
            cms_page.cascadepage.glossary['element_ids'].setdefault(obj.language, {})
            cms_page.cascadepage.glossary['element_ids'][obj.language][str(obj.pk)] = element_id
            cms_page.cascadepage.save()


================================================
FILE: cmsplugin_cascade/generic/settings.py
================================================
CASCADE_PLUGINS = ['custom_snippet', 'heading', 'horizontal_rule', 'simple_wrapper', 'text_image']

def set_defaults(config):
    from cmsplugin_cascade.extra_fields.config import PluginExtraFieldsConfig

    config.setdefault('plugins_with_extra_fields', {})
    plugins_with_extra_fields = config['plugins_with_extra_fields']
    plugins_with_extra_fields.setdefault('HorizontalRulePlugin', PluginExtraFieldsConfig(
        inline_styles={
            'extra_fields:Border': ['border-top'],
            'extra_fields:Border Radius': ['border-radius'],
            'extra_units:Border Radius': 'px,rem',
        },
        allow_override=False,
    ))


================================================
FILE: cmsplugin_cascade/generic/simple_wrapper.py
================================================
from django.forms import ChoiceField
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from cms.plugin_pool import plugin_pool
from entangled.forms import EntangledModelFormMixin

from cmsplugin_cascade.plugin_base import CascadePluginBase, TransparentContainer


class SimpleWrapperFormMixin(EntangledModelFormMixin):
    TAG_CHOICES = [(cls, _("<{}> – Element").format(cls)) for cls in ['div', 'span', 'section', 'a
Download .txt
gitextract_301603hg/

├── .coveragerc
├── .editorconfig
├── .github/
│   └── workflows/
│       ├── publish.yml
│       └── tests.yml
├── .gitignore
├── BOOSTRAP-4.md
├── LICENSE-MIT
├── MANIFEST.in
├── README.md
├── cmsplugin_cascade/
│   ├── __init__.py
│   ├── admin.py
│   ├── app_settings.py
│   ├── apps.py
│   ├── bootstrap4/
│   │   ├── __init__.py
│   │   ├── accordion.py
│   │   ├── buttons.py
│   │   ├── card.py
│   │   ├── carousel.py
│   │   ├── container.py
│   │   ├── embeds.py
│   │   ├── fields.py
│   │   ├── grid.py
│   │   ├── icon.py
│   │   ├── image.py
│   │   ├── jumbotron.py
│   │   ├── mixins.py
│   │   ├── picture.py
│   │   ├── plugin_base.py
│   │   ├── secondary_menu.py
│   │   ├── settings.py
│   │   ├── tabs.py
│   │   └── utils.py
│   ├── clipboard/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── cms_plugins.py
│   │   ├── cms_toolbars.py
│   │   ├── forms.py
│   │   └── utils.py
│   ├── cms_plugins.py
│   ├── cms_toolbars.py
│   ├── extra_fields/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── config.py
│   │   └── mixins.py
│   ├── fields.py
│   ├── forms.py
│   ├── generic/
│   │   ├── __init__.py
│   │   ├── custom_snippet.py
│   │   ├── heading.py
│   │   ├── horizontal_rule.py
│   │   ├── mixins.py
│   │   ├── settings.py
│   │   ├── simple_wrapper.py
│   │   └── text_image.py
│   ├── hide_plugins.py
│   ├── icon/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── forms.py
│   │   ├── plugin_base.py
│   │   ├── settings.py
│   │   ├── simpleicon.py
│   │   ├── texticon.py
│   │   └── utils.py
│   ├── image.py
│   ├── leaflet/
│   │   ├── __init__.py
│   │   ├── map.py
│   │   └── settings.py
│   ├── link/
│   │   ├── __init__.py
│   │   ├── cms_plugins.py
│   │   ├── config.py
│   │   ├── forms.py
│   │   └── plugin_base.py
│   ├── locale/
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   └── uk/
│   │       └── LC_MESSAGES/
│   │           └── django.po
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_auto_20150530_1018.py
│   │   ├── 0003_inlinecascadeelement.py
│   │   ├── 0004_auto_20151112_0147.py
│   │   ├── 0005_tabset_and_clipboard.py
│   │   ├── 0006_bootstrapgallerypluginmodel.py
│   │   ├── 0007_add_proxy_models.py
│   │   ├── 0008_sortableinlinecascadeelement.py
│   │   ├── 0009_cascadepage.py
│   │   ├── 0010_refactor_heading.py
│   │   ├── 0011_merge_sharable_with_cascadeelement.py
│   │   ├── 0012_auto_20160619_1854.py
│   │   ├── 0013_iconfont.py
│   │   ├── 0014_glossary_field.py
│   │   ├── 0015_carousel_slide.py
│   │   ├── 0016_shared_glossary_uneditable.py
│   │   ├── 0017_fake_proxy_models.py
│   │   ├── 0018_iconfont_color.py
│   │   ├── 0019_verbose_table_names.py
│   │   ├── 0020_page_icon_font.py
│   │   ├── 0021_cascadepage_verbose_name.py
│   │   ├── 0022_auto_20181202_1055.py
│   │   ├── 0023_iconfont_is_default.py
│   │   ├── 0024_page_icon_font.py
│   │   ├── 0025_texteditorconfigfields.py
│   │   ├── 0026_cascadepage_menu_symbol.py
│   │   ├── 0027_version_1.py
│   │   ├── 0028_cascade_clipboard.py
│   │   ├── 0029_json_field.py
│   │   ├── 0030_separate_ids_per_language.py
│   │   ├── 0031_alter_texteditorconfigfields_element_type.py
│   │   └── __init__.py
│   ├── mixins.py
│   ├── models.py
│   ├── models_base.py
│   ├── plugin_base.py
│   ├── render_template.py
│   ├── segmentation/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── cms_plugins.py
│   │   ├── cms_toolbars.py
│   │   └── mixins.py
│   ├── sharable/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── fields.py
│   │   └── forms.py
│   ├── sphinx/
│   │   ├── __init__.py
│   │   ├── cms_apps.py
│   │   ├── cms_menus.py
│   │   ├── fragmentsbuilder.py
│   │   ├── link_plugin.py
│   │   ├── static/
│   │   │   └── cascade/
│   │   │       └── sphinx/
│   │   │           ├── css/
│   │   │           │   ├── bootstrap-sphinx.css
│   │   │           │   └── documentation.css
│   │   │           └── js/
│   │   │               └── link_plugin.js
│   │   └── theme/
│   │       └── bootstrap-fragments/
│   │           ├── globaltoc.html
│   │           ├── layout.html
│   │           └── theme.conf
│   ├── static/
│   │   └── cascade/
│   │       ├── LICENSE.md
│   │       ├── css/
│   │       │   └── admin/
│   │       │       ├── bootstrap4-buttons.css
│   │       │       ├── borderchoice.css
│   │       │       ├── cascadepage.css
│   │       │       ├── clipboard.css
│   │       │       ├── colorpicker.css
│   │       │       ├── editplugin.css
│   │       │       ├── iconfont.css
│   │       │       ├── iconplugin.css
│   │       │       ├── leafletplugin.css
│   │       │       ├── linkplugin.css
│   │       │       └── partialfields.css
│   │       └── js/
│   │           ├── admin/
│   │           │   ├── buttonmixin.js
│   │           │   ├── buttonplugin.js
│   │           │   ├── cascadepage.js
│   │           │   ├── clipboard.js
│   │           │   ├── colorpicker.js
│   │           │   ├── framediconplugin.js
│   │           │   ├── iconplugin.js
│   │           │   ├── iconpluginmixin.js
│   │           │   ├── imageplugin.js
│   │           │   ├── jumbotronplugin.js
│   │           │   ├── leafletplugin.js
│   │           │   ├── linkplugin.js
│   │           │   ├── pictureplugin.js
│   │           │   ├── segmentation.js
│   │           │   ├── segmentplugin.js
│   │           │   ├── sharableglossary.js
│   │           │   ├── sharedsettingsfield.js
│   │           │   ├── textimageplugin.js
│   │           │   └── textlinkplugin.js
│   │           ├── picturefill.js
│   │           ├── ring.js
│   │           └── underscore.js
│   ├── strides.py
│   ├── templates/
│   │   └── cascade/
│   │       ├── admin/
│   │       │   ├── change_form.html
│   │       │   ├── ckeditor.wysiwyg.txt
│   │       │   ├── clipboard_close_frame.html
│   │       │   ├── clipboard_paste_plugins.html
│   │       │   ├── clipboard_reload_page.html
│   │       │   ├── leaflet_plugin_change_form.html
│   │       │   ├── legacy_widgets/
│   │       │   │   ├── button_sizes.html
│   │       │   │   ├── button_types.html
│   │       │   │   ├── container_breakpoints.html
│   │       │   │   └── panel_types.html
│   │       │   ├── segmentation_list.html
│   │       │   ├── sharedglossary_change_form.html
│   │       │   └── widgets/
│   │       │       ├── borderchoice.html
│   │       │       ├── button_sizes.html
│   │       │       ├── button_types.html
│   │       │       ├── colorpicker.html
│   │       │       ├── container_breakpoints.html
│   │       │       ├── inherit_color.html
│   │       │       └── panel_types.html
│   │       ├── bootstrap4/
│   │       │   ├── accordion.html
│   │       │   ├── angular-ui/
│   │       │   │   ├── accordion.html
│   │       │   │   ├── carousel.html
│   │       │   │   └── tabset.html
│   │       │   ├── button.html
│   │       │   ├── card.html
│   │       │   ├── carousel-slide.html
│   │       │   ├── carousel.html
│   │       │   ├── framedicon.html
│   │       │   ├── image.html
│   │       │   ├── jumbotron.html
│   │       │   ├── linked-image.html
│   │       │   ├── linked-picture.html
│   │       │   ├── picture.html
│   │       │   ├── secmenu-list-group.html
│   │       │   ├── secmenu-list-item.html
│   │       │   ├── secmenu-unstyled-list-item.html
│   │       │   ├── secmenu-unstyled-list.html
│   │       │   ├── tabset.html
│   │       │   └── youtube.html
│   │       ├── generic/
│   │       │   ├── does_not_exist.html
│   │       │   ├── heading.html
│   │       │   ├── naked.html
│   │       │   ├── single.html
│   │       │   └── wrapper.html
│   │       ├── link/
│   │       │   ├── .editorconfig
│   │       │   ├── link-base-nostyle.html
│   │       │   ├── link-base.html
│   │       │   ├── text-link-linebreak.html
│   │       │   └── text-link.html
│   │       └── plugins/
│   │           ├── googlemap.html
│   │           ├── leaflet.html
│   │           ├── link.html
│   │           ├── simpleicon.html
│   │           ├── texticon.html
│   │           └── textimage.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── cascade_tags.py
│   ├── utils.py
│   └── widgets.py
├── docs/
│   ├── Makefile
│   ├── make.bat
│   └── source/
│       ├── _static/
│       │   └── shop_link_plugin.py
│       ├── bootstrap3/
│       │   ├── gallery.rst
│       │   ├── grid.rst
│       │   ├── image-picture.rst
│       │   ├── index.rst
│       │   ├── navbar.rst
│       │   └── other-components.rst
│       ├── bootstrap4/
│       │   ├── grid.rst
│       │   ├── index.rst
│       │   └── utilities.rst
│       ├── changelog.rst
│       ├── client-side.rst
│       ├── clipboard.rst
│       ├── conf.py
│       ├── customize-styles.rst
│       ├── customized-plugins.rst
│       ├── embeds.rst
│       ├── generic-plugins.rst
│       ├── hide-plugins.rst
│       ├── icon-fonts.rst
│       ├── impatient.rst
│       ├── index.rst
│       ├── installation.rst
│       ├── introduction.rst
│       ├── leaflet.rst
│       ├── link-plugin.rst
│       ├── release-notes-1.rst
│       ├── render-template.rst
│       ├── section.rst
│       ├── segmentation.rst
│       ├── sharable-fields.rst
│       ├── sphinx.rst
│       └── strides.rst
├── examples/
│   └── bs4demo/
│       ├── .coveragerc
│       ├── bs4demo/
│       │   ├── __init__.py
│       │   ├── cms_plugins.py
│       │   ├── context_processors.py
│       │   ├── models.py
│       │   ├── settings.py
│       │   ├── static/
│       │   │   └── bs4demo/
│       │   │       ├── cascades/
│       │   │       │   └── strides.json
│       │   │       └── css/
│       │   │           ├── _footer.scss
│       │   │           ├── _variables.scss
│       │   │           ├── badge.scss
│       │   │           └── main.scss
│       │   ├── templates/
│       │   │   └── bs4demo/
│       │   │       ├── badge.html
│       │   │       ├── base.html
│       │   │       ├── main.html
│       │   │       ├── strides.html
│       │   │       └── wrapped.html
│       │   ├── urls.py
│       │   └── utils.py
│       ├── manage.py
│       └── package.json
├── pytest.ini
├── requirements/
│   └── base.txt
├── setup.py
└── tests/
    ├── __init__.py
    ├── bootstrap4/
    │   ├── __init__.py
    │   ├── conftest.py
    │   ├── test_accordion.py
    │   ├── test_container.py
    │   └── test_grid.py
    ├── conftest.py
    ├── requirements.txt
    ├── settings.py
    ├── static/
    │   ├── strides/
    │   │   ├── bootstrap-button.json
    │   │   ├── bootstrap-column.json
    │   │   ├── bootstrap-container.json
    │   │   ├── bootstrap-jumbotron.json
    │   │   ├── bootstrap-row.json
    │   │   ├── carousel-plugin.json
    │   │   ├── framed-icon.json
    │   │   ├── simple-wrapper.json
    │   │   └── text-plugin.json
    │   └── strides.json
    ├── templates/
    │   └── testing.html
    ├── test_base.py
    ├── test_customplugin.py
    ├── test_http.py
    ├── test_iconfont.py
    ├── test_missingmigrations.py
    ├── test_segmentation.py
    ├── test_strides.py
    ├── urls.py
    └── utils.py
Download .txt
SYMBOL INDEX (866 symbols across 128 files)

FILE: cmsplugin_cascade/admin.py
  class CascadePageAdmin (line 20) | class CascadePageAdmin(PageExtensionAdmin):
    method media (line 25) | def media(self):
    method get_form (line 31) | def get_form(self, request, obj=None, **kwargs):
    method get_urls (line 36) | def get_urls(self):
    method get_page_sections (line 50) | def get_page_sections(self, request, page_pk=None):
    method get_published_pagelist (line 63) | def get_published_pagelist(self, request, *args, **kwargs):
    method get_result_set (line 102) | def get_result_set(self, language, page):
    method fetch_fonticons (line 110) | def fetch_fonticons(self, request, iconfont_id=None):
    method validate_exturl (line 121) | def validate_exturl(self, request):
    method changeform_view (line 136) | def changeform_view(self, request, object_id=None, form_url='', extra_...

FILE: cmsplugin_cascade/app_settings.py
  class AppSettings (line 2) | class AppSettings:
    method _setting (line 4) | def _setting(self, name, default=None):
    method CASCADE_PLUGINS (line 9) | def CASCADE_PLUGINS(self):
    method CMSPLUGIN_CASCADE (line 17) | def CMSPLUGIN_CASCADE(self):
    method CSS_PREFIXES (line 149) | def CSS_PREFIXES(self):
    method RESPONSIVE_IMAGE_MAX_STEPS (line 155) | def RESPONSIVE_IMAGE_MAX_STEPS(self):
    method RESPONSIVE_IMAGE_STEP_SIZE (line 163) | def RESPONSIVE_IMAGE_STEP_SIZE(self):

FILE: cmsplugin_cascade/apps.py
  class CascadeConfig (line 10) | class CascadeConfig(AppConfig):
    method ready (line 15) | def ready(self):
    method pre_migrate (line 28) | def pre_migrate(cls, sender=None, **kwargs):
    method post_migrate (line 44) | def post_migrate(cls, sender=None, **kwargs):
    method get_proxy_models (line 56) | def get_proxy_models(self):
    method grant_permissions (line 66) | def grant_permissions(self, proxy_model):
    method revoke_permissions (line 96) | def revoke_permissions(self, ctype):

FILE: cmsplugin_cascade/bootstrap4/accordion.py
  class AccordionFormMixin (line 15) | class AccordionFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin):
    class Meta (line 38) | class Meta:
  class BootstrapAccordionPlugin (line 43) | class BootstrapAccordionPlugin(TransparentWrapper, BootstrapPluginBase):
    method get_identifier (line 54) | def get_identifier(cls, obj):
    method render (line 59) | def render(self, context, instance, placeholder):
    method save_model (line 67) | def save_model(self, request, obj, form, change):
  class AccordionGroupFormMixin (line 75) | class AccordionGroupFormMixin(EntangledModelFormMixin):
    class Meta (line 88) | class Meta:
    method clean_heading (line 91) | def clean_heading(self):
  class BootstrapAccordionGroupPlugin (line 95) | class BootstrapAccordionGroupPlugin(TransparentContainer, BootstrapPlugi...
    method get_identifier (line 104) | def get_identifier(cls, instance):
    method render (line 108) | def render(self, context, instance, placeholder):

FILE: cmsplugin_cascade/bootstrap4/buttons.py
  class ButtonTypeWidget (line 13) | class ButtonTypeWidget(widgets.RadioSelect):
  class ButtonSizeWidget (line 20) | class ButtonSizeWidget(widgets.RadioSelect):
  class ButtonFormMixin (line 27) | class ButtonFormMixin(EntangledModelFormMixin):
    class Meta (line 106) | class Meta:
  class BootstrapButtonMixin (line 111) | class BootstrapButtonMixin(IconPluginMixin):
    class Media (line 120) | class Media:
    method render (line 124) | def render(self, context, instance, placeholder):
  class BootstrapButtonFormMixin (line 138) | class BootstrapButtonFormMixin(LinkFormMixin, IconFormMixin, ButtonFormM...
  class BootstrapButtonPlugin (line 143) | class BootstrapButtonPlugin(BootstrapButtonMixin, LinkPluginBase):
    class Media (line 151) | class Media:
    method get_identifier (line 155) | def get_identifier(cls, instance):
    method get_css_classes (line 166) | def get_css_classes(cls, obj):
    method get_html_tag_attributes (line 173) | def get_html_tag_attributes(cls, obj):
    method render (line 178) | def render(self, context, instance, placeholder):

FILE: cmsplugin_cascade/bootstrap4/card.py
  class CardChildBase (line 7) | class CardChildBase(BootstrapPluginBase):
  class BootstrapCardHeaderPlugin (line 15) | class BootstrapCardHeaderPlugin(TransparentContainer, CardChildBase):
  class BootstrapCardBodyPlugin (line 22) | class BootstrapCardBodyPlugin(TransparentContainer, CardChildBase):
  class BootstrapCardFooterPlugin (line 29) | class BootstrapCardFooterPlugin(TransparentContainer, CardChildBase):
  class BootstrapCardPlugin (line 36) | class BootstrapCardPlugin(TransparentWrapper, BootstrapPluginBase):
    method get_identifier (line 48) | def get_identifier(cls, instance):
    method get_child_classes (line 56) | def get_child_classes(cls, slot, page, instance=None):

FILE: cmsplugin_cascade/bootstrap4/carousel.py
  class CarouselSlidesFormMixin (line 21) | class CarouselSlidesFormMixin(ManageChildrenFormMixin, EntangledModelFor...
    class Meta (line 58) | class Meta:
  class BootstrapCarouselPlugin (line 63) | class BootstrapCarouselPlugin(BootstrapPluginBase):
    method get_identifier (line 74) | def get_identifier(cls, obj):
    method get_css_classes (line 80) | def get_css_classes(cls, obj):
    method get_html_tag_attributes (line 87) | def get_html_tag_attributes(cls, obj):
    method save_model (line 96) | def save_model(self, request, obj, form, change):
    method sanitize_model (line 103) | def sanitize_model(cls, obj):
  class BootstrapCarouselSlidePlugin (line 117) | class BootstrapCarouselSlidePlugin(BootstrapPluginBase):
    method render (line 128) | def render(self, context, instance, placeholder):
    method sanitize_model (line 145) | def sanitize_model(cls, obj):
    method get_identifier (line 171) | def get_identifier(cls, obj):

FILE: cmsplugin_cascade/bootstrap4/container.py
  function get_widget_choices (line 18) | def get_widget_choices():
  class ContainerBreakpointsWidget (line 31) | class ContainerBreakpointsWidget(widgets.CheckboxSelectMultiple):
  class ContainerFormMixin (line 35) | class ContainerFormMixin(EntangledModelFormMixin):
    class Meta (line 51) | class Meta:
    method clean_breapoints (line 54) | def clean_breapoints(self):
  class ContainerGridMixin (line 61) | class ContainerGridMixin:
    method get_grid_instance (line 62) | def get_grid_instance(self):
  class BootstrapContainerPlugin (line 75) | class BootstrapContainerPlugin(BootstrapPluginBase):
    method get_identifier (line 87) | def get_identifier(cls, obj):
    method get_css_classes (line 97) | def get_css_classes(cls, obj):
    method save_model (line 105) | def save_model(self, request, obj, form, change):
  class BootstrapRowFormMixin (line 112) | class BootstrapRowFormMixin(ManageChildrenFormMixin, EntangledModelFormM...
    class Meta (line 124) | class Meta:
  class RowGridMixin (line 128) | class RowGridMixin:
    method get_grid_instance (line 129) | def get_grid_instance(self):
  class BootstrapRowPlugin (line 138) | class BootstrapRowPlugin(BootstrapPluginBase):
    method get_identifier (line 146) | def get_identifier(cls, obj):
    method save_model (line 151) | def save_model(self, request, obj, form, change):
  class ColumnGridMixin (line 160) | class ColumnGridMixin:
    method get_grid_instance (line 163) | def get_grid_instance(self):
  class BootstrapColumnPlugin (line 180) | class BootstrapColumnPlugin(BootstrapPluginBase):
    method get_form (line 189) | def get_form(self, request, obj=None, **kwargs):
    method save_model (line 336) | def save_model(self, request, obj, form, change):
    method sanitize_model (line 341) | def sanitize_model(cls, obj):
    method get_identifier (line 346) | def get_identifier(cls, obj):

FILE: cmsplugin_cascade/bootstrap4/embeds.py
  class YoutubeFormMixin (line 13) | class YoutubeFormMixin(EntangledModelFormMixin):
    class Meta (line 62) | class Meta:
    method __init__ (line 67) | def __init__(self, *args, **kwargs):
    method clean (line 77) | def clean(self):
  class BootstrapYoutubePlugin (line 93) | class BootstrapYoutubePlugin(BootstrapPluginBase):
    method render (line 104) | def render(self, context, instance, placeholder):
    method get_css_classes (line 118) | def get_css_classes(cls, obj):
    method get_identifier (line 127) | def get_identifier(cls, obj):

FILE: cmsplugin_cascade/bootstrap4/fields.py
  class BootstrapMultiSizeField (line 5) | class BootstrapMultiSizeField(MultiSizeField):
    method __init__ (line 10) | def __init__(self, *args, **kwargs):

FILE: cmsplugin_cascade/bootstrap4/grid.py
  class BootstrapException (line 11) | class BootstrapException(Exception):
  class Breakpoint (line 18) | class Breakpoint(Enum):
    method range (line 29) | def range(cls, first, last):
    method __gt__ (line 38) | def __gt__(self, other):
    method __ge__ (line 41) | def __ge__(self, other):
    method __lt__ (line 44) | def __lt__(self, other):
    method __le__ (line 47) | def __le__(self, other):
    method __iter__ (line 50) | def __iter__(self):
    method label (line 58) | def label(self):
    method media_query (line 68) | def media_query(self):
  class Bound (line 78) | class Bound:
    method __init__ (line 79) | def __init__(self, min, max):
    method __copy__ (line 83) | def __copy__(self):
    method __eq__ (line 86) | def __eq__(self, other):
    method __add__ (line 89) | def __add__(self, other):
    method __sub__ (line 95) | def __sub__(self, other):
    method __repr__ (line 101) | def __repr__(self):
    method extend (line 104) | def extend(self, other):
  class Break (line 126) | class Break:
    method __init__ (line 127) | def __init__(self, breakpoint, classes, narrower=None):
    method _normalize_col_classes (line 137) | def _normalize_col_classes(self, classes):
    method _inherit_from (line 171) | def _inherit_from(self, narrower):
    method __copy__ (line 179) | def __copy__(self):
    method __repr__ (line 185) | def __repr__(self):
  class Bootstrap4Container (line 190) | class Bootstrap4Container(list):
    method __init__ (line 198) | def __init__(self, bounds=default_bounds):
    method __repr__ (line 201) | def __repr__(self):
    method add_row (line 204) | def add_row(self, row):
  class Bootstrap4Row (line 215) | class Bootstrap4Row(list):
    method __repr__ (line 224) | def __repr__(self):
    method add_column (line 227) | def add_column(self, column):
    method compute_column_bounds (line 234) | def compute_column_bounds(self):
  class Bootstrap4Column (line 272) | class Bootstrap4Column(list):
    method __init__ (line 281) | def __init__(self, classes=[]):
    method __repr__ (line 290) | def __repr__(self):
    method __copy__ (line 293) | def __copy__(self):
    method add_row (line 298) | def add_row(self, row):
    method get_bound (line 309) | def get_bound(self, breakpoint):
    method get_min_max_bounds (line 315) | def get_min_max_bounds(self):

FILE: cmsplugin_cascade/bootstrap4/icon.py
  class FramedIconFormMixin (line 13) | class FramedIconFormMixin(IconFormMixin):
    class Meta (line 59) | class Meta:
  class FramedIconPlugin (line 64) | class FramedIconPlugin(IconPluginMixin, LinkPluginBase):
    class Media (line 74) | class Media:
    method get_tag_type (line 78) | def get_tag_type(self, instance):
    method get_css_classes (line 83) | def get_css_classes(cls, instance):
    method get_inline_styles (line 91) | def get_inline_styles(cls, instance):
    method render (line 96) | def render(self, context, instance, placeholder):

FILE: cmsplugin_cascade/bootstrap4/image.py
  class BootstrapImageFormMixin (line 17) | class BootstrapImageFormMixin(ImageFormMixin):
    class Meta (line 70) | class Meta:
  class BootstrapImagePlugin (line 75) | class BootstrapImagePlugin(LinkPluginBase):
    class Media (line 91) | class Media:
    method render (line 94) | def render(self, context, instance, placeholder):
    method get_css_classes (line 110) | def get_css_classes(cls, obj):
    method get_identifier (line 118) | def get_identifier(cls, obj):
    method sanitize_model (line 126) | def sanitize_model(cls, obj):

FILE: cmsplugin_cascade/bootstrap4/jumbotron.py
  class ImageBackgroundMixin (line 22) | class ImageBackgroundMixin:
    method element_heights (line 24) | def element_heights(self):
    method background_color (line 31) | def background_color(self):
    method background_attachment (line 41) | def background_attachment(self):
    method background_position (line 48) | def background_position(self):
    method background_repeat (line 55) | def background_repeat(self):
    method background_size (line 62) | def background_size(self):
  class JumbotronFormMixin (line 75) | class JumbotronFormMixin(EntangledModelFormMixin):
    class Meta (line 161) | class Meta:
    method validate_optional_field (line 167) | def validate_optional_field(self, name):
    method clean (line 175) | def clean(self):
  class BootstrapJumbotronPlugin (line 192) | class BootstrapJumbotronPlugin(BootstrapPluginBase):
    class Media (line 208) | class Media:
    method render (line 211) | def render(self, context, instance, placeholder):
    method sanitize_model (line 231) | def sanitize_model(cls, obj):
    method get_css_classes (line 248) | def get_css_classes(cls, obj):
    method get_identifier (line 257) | def get_identifier(cls, obj):

FILE: cmsplugin_cascade/bootstrap4/mixins.py
  class BootstrapUtilities (line 9) | class BootstrapUtilities:
    method __new__ (line 32) | def __new__(cls, *args):
    method background_and_color (line 45) | def background_and_color(cls):
    method margins (line 68) | def margins(cls):
    method vertical_margins (line 98) | def vertical_margins(cls):
    method paddings (line 124) | def paddings(cls):
    method floats (line 154) | def floats(cls):

FILE: cmsplugin_cascade/bootstrap4/picture.py
  class BootstrapPictureFormMixin (line 17) | class BootstrapPictureFormMixin(ImageFormMixin):
    class Meta (line 51) | class Meta:
  class BootstrapPicturePlugin (line 55) | class BootstrapPicturePlugin(LinkPluginBase):
    class Media (line 72) | class Media:
    method render (line 75) | def render(self, context, instance, placeholder):
    method get_css_classes (line 92) | def get_css_classes(cls, obj):
    method get_identifier (line 100) | def get_identifier(cls, obj):
    method sanitize_model (line 108) | def sanitize_model(cls, obj):

FILE: cmsplugin_cascade/bootstrap4/plugin_base.py
  class BootstrapPluginBase (line 7) | class BootstrapPluginBase(CascadePluginBase):
    method get_render_template (line 13) | def get_render_template(self, context, instance, placeholder):

FILE: cmsplugin_cascade/bootstrap4/secondary_menu.py
  class SecondaryMenuFormMixin (line 10) | class SecondaryMenuFormMixin(EntangledModelFormMixin):
    class Meta (line 28) | class Meta:
    method __init__ (line 31) | def __init__(self, *args, **kwargs):
  class BootstrapSecondaryMenuPlugin (line 37) | class BootstrapSecondaryMenuPlugin(BootstrapPluginBase):
    method get_identifier (line 51) | def get_identifier(cls, obj):
    method render (line 54) | def render(self, context, instance, placeholder):
    method sanitize_model (line 64) | def sanitize_model(cls, instance):

FILE: cmsplugin_cascade/bootstrap4/settings.py
  function set_defaults (line 15) | def set_defaults(config):

FILE: cmsplugin_cascade/bootstrap4/tabs.py
  class TabSetFormMixin (line 16) | class TabSetFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin):
    class Meta (line 30) | class Meta:
  class BootstrapTabSetPlugin (line 35) | class BootstrapTabSetPlugin(TransparentWrapper, BootstrapPluginBase):
    method get_identifier (line 46) | def get_identifier(cls, instance):
    method get_css_classes (line 52) | def get_css_classes(cls, obj):
    method save_model (line 58) | def save_model(self, request, obj, form, change):
  class TabPaneFormMixin (line 66) | class TabPaneFormMixin(EntangledModelFormMixin):
    class Meta (line 72) | class Meta:
  class BootstrapTabPanePlugin (line 76) | class BootstrapTabPanePlugin(TransparentContainer, BootstrapPluginBase):
    method get_identifier (line 85) | def get_identifier(cls, obj):

FILE: cmsplugin_cascade/bootstrap4/utils.py
  function get_image_tags (line 25) | def get_image_tags(instance):
  function get_picture_elements (line 91) | def get_picture_elements(instance):

FILE: cmsplugin_cascade/clipboard/admin.py
  class JSONAdminWidget (line 16) | class JSONAdminWidget(widgets.Textarea):
    method __init__ (line 17) | def __init__(self):
    method render (line 21) | def render(self, name, value, attrs=None, renderer=None):
  class CascadeClipboardAdmin (line 40) | class CascadeClipboardAdmin(admin.ModelAdmin):
    class Media (line 48) | class Media:
    method save_clipboard (line 52) | def save_clipboard(self, obj):
    method restore_clipboard (line 57) | def restore_clipboard(self, obj):
    method save_model (line 62) | def save_model(self, request, obj, form, change):

FILE: cmsplugin_cascade/clipboard/cms_plugins.py
  class CascadeClipboardPlugin (line 22) | class CascadeClipboardPlugin(CMSPluginBase):
    method get_plugin_urls (line 26) | def get_plugin_urls(self):
    method get_extra_placeholder_menu_items (line 34) | def get_extra_placeholder_menu_items(cls, request, placeholder):
    method render_modal_window (line 60) | def render_modal_window(self, request, form):
    method import_plugins_view (line 84) | def import_plugins_view(self, request):
    method paste_from_clipboard (line 112) | def paste_from_clipboard(self, request, form):
    method export_plugins_view (line 145) | def export_plugins_view(self, request):
    method add_to_clipboard (line 167) | def add_to_clipboard(self, request, form):

FILE: cmsplugin_cascade/clipboard/cms_toolbars.py
  class CascadeClipboardToolbar (line 6) | class CascadeClipboardToolbar(CMSToolbar):
    class Media (line 7) | class Media:

FILE: cmsplugin_cascade/clipboard/forms.py
  class ClipboardBaseForm (line 10) | class ClipboardBaseForm(forms.Form):
    method clean_identifier (line 23) | def clean_identifier(self):

FILE: cmsplugin_cascade/clipboard/utils.py
  function serialize_from_placeholder (line 15) | def serialize_from_placeholder(placeholder, admin_site=default_admin_site):
  function deserialize_to_clipboard (line 39) | def deserialize_to_clipboard(request, data):

FILE: cmsplugin_cascade/cms_toolbars.py
  class CascadePageToolbar (line 10) | class CascadePageToolbar(ExtensionToolbar):
    method populate (line 13) | def populate(self):

FILE: cmsplugin_cascade/extra_fields/admin.py
  class PluginExtraFieldsForm (line 24) | class PluginExtraFieldsForm(EntangledModelForm):
    class Meta (line 40) | class Meta:
    method clean_class_names (line 47) | def clean_class_names(self):
  class PluginExtraFieldsAdmin (line 57) | class PluginExtraFieldsAdmin(admin.ModelAdmin):
    class Media (line 82) | class Media:
    method plugins_for_site (line 86) | def plugins_for_site(self):
    method get_form (line 99) | def get_form(self, request, obj=None, **kwargs):
    method has_add_permission (line 125) | def has_add_permission(self, request):
    method module (line 132) | def module(self, obj):
    method allowed_classes_styles (line 136) | def allowed_classes_styles(self, obj):
  class TextEditorConfigForm (line 145) | class TextEditorConfigForm(ModelForm):
    class Meta (line 148) | class Meta:
    method clean_css_classes (line 151) | def clean_css_classes(self):
  class TextEditorConfigAdmin (line 162) | class TextEditorConfigAdmin(admin.ModelAdmin):
    method get_urls (line 166) | def get_urls(self):
    method render_texteditor_config (line 172) | def render_texteditor_config(self, request):

FILE: cmsplugin_cascade/extra_fields/config.py
  class PluginExtraFieldsConfig (line 2) | class PluginExtraFieldsConfig:
    method __init__ (line 25) | def __init__(self, allow_id_tag=False, css_classes=None, inline_styles...

FILE: cmsplugin_cascade/extra_fields/mixins.py
  class ExtraFieldsMixin (line 12) | class ExtraFieldsMixin(metaclass=MediaDefiningClass):
    method __str__ (line 19) | def __str__(self):
    method get_form (line 22) | def get_form(self, request, obj=None, **kwargs):
    method get_css_classes (line 92) | def get_css_classes(cls, obj):
    method get_inline_styles (line 104) | def get_inline_styles(cls, obj):
    method get_html_tag_attributes (line 125) | def get_html_tag_attributes(cls, obj):
    method get_identifier (line 133) | def get_identifier(cls, obj):

FILE: cmsplugin_cascade/fields.py
  class GlossaryField (line 21) | class GlossaryField:
    method __init__ (line 27) | def __init__(self, widget, label=None, name=None, initial='', help_tex...
    method run_validators (line 40) | def run_validators(self, value):
    method get_element_ids (line 63) | def get_element_ids(self, prefix_id):
  class BorderChoiceField (line 76) | class BorderChoiceField(MultiValueField):
    method __init__ (line 80) | def __init__(self, *args, **kwargs):
    method prepare_value (line 92) | def prepare_value(self, value):
    method compress (line 95) | def compress(self, data_list):
    method css_value (line 99) | def css_value(self, values):
  class SelectTextAlignField (line 103) | class SelectTextAlignField(ChoiceField):
    method __init__ (line 106) | def __init__(self, *args, **kwargs):
  class SelectOverflowField (line 110) | class SelectOverflowField(ChoiceField):
    method __init__ (line 113) | def __init__(self, *args, **kwargs):
  class ColorValidator (line 118) | class ColorValidator():
    method __init__ (line 122) | def __init__(self, with_alpha):
    method __call__ (line 128) | def __call__(self, value):
    method __eq__ (line 135) | def __eq__(self, other):
  class ColorField (line 144) | class ColorField(MultiValueField):
    method __init__ (line 147) | def __init__(self, inherit_color=False, default_color=DEFAULT_COLOR, *...
    method compress (line 160) | def compress(self, data_list):
    method css_value (line 165) | def css_value(self, values):
  class SizeUnitValidator (line 170) | class SizeUnitValidator():
    method __init__ (line 175) | def __init__(self, allowed_units=None, allow_negative=True):
    method __call__ (line 193) | def __call__(self, value):
    method __eq__ (line 204) | def __eq__(self, other):
  class SizeField (line 213) | class SizeField(Field):
    method __init__ (line 218) | def __init__(self, *, allowed_units=None, **kwargs):
    method to_python (line 224) | def to_python(self, value):
  class MultiSizeField (line 233) | class MultiSizeField(MultiValueField):
    method __init__ (line 238) | def __init__(self, properties, sublabels=None, *args, **kwargs):
    method prepare_value (line 257) | def prepare_value(self, value):
    method compress (line 263) | def compress(self, data_list):
  class HiddenDictField (line 268) | class HiddenDictField(Field):
    method prepare_value (line 271) | def prepare_value(self, value):
    method clean (line 277) | def clean(self, value):
  class CascadeImageField (line 284) | class CascadeImageField(AdminImageFormField):
    method __init__ (line 285) | def __init__(self, *args, **kwargs):

FILE: cmsplugin_cascade/forms.py
  class ManageChildrenFormMixin (line 7) | class ManageChildrenFormMixin:
    class Meta (line 14) | class Meta:
    method __init__ (line 17) | def __init__(self, *args, **kwargs):
  class CascadeModelFormMixin (line 25) | class CascadeModelFormMixin(EntangledModelFormMixin):
    method _clean_form (line 30) | def _clean_form(self):
  class CascadeModelForm (line 37) | class CascadeModelForm(CascadeModelFormMixin, ModelForm):

FILE: cmsplugin_cascade/generic/custom_snippet.py
  class CustomSnippetPlugin (line 9) | class CustomSnippetPlugin(TransparentContainer, CascadePluginBase):
    method get_identifier (line 22) | def get_identifier(cls, instance):

FILE: cmsplugin_cascade/generic/heading.py
  class HeadingFormMixin (line 11) | class HeadingFormMixin(EntangledModelFormMixin):
    class Meta (line 24) | class Meta:
  class HeadingPlugin (line 28) | class HeadingPlugin(CascadePluginBase):
    method get_identifier (line 36) | def get_identifier(cls, instance):
    method render (line 43) | def render(self, context, instance, placeholder):

FILE: cmsplugin_cascade/generic/horizontal_rule.py
  class HorizontalRulePlugin (line 7) | class HorizontalRulePlugin(CascadePluginBase):

FILE: cmsplugin_cascade/generic/mixins.py
  class SectionFormMixin (line 11) | class SectionFormMixin(EntangledModelFormMixin):
    class Meta (line 19) | class Meta:
    method clean_element_id (line 22) | def clean_element_id(self):
    method check_unique_element_id (line 28) | def check_unique_element_id(cls, instance, element_id):
  class SectionModelMixin (line 46) | class SectionModelMixin:
    method element_id (line 47) | def element_id(self):
    method delete (line 52) | def delete(self, *args, **kwargs):
  class SectionMixin (line 62) | class SectionMixin:
    method get_form (line 63) | def get_form(self, request, obj=None, **kwargs):
    method get_identifier (line 70) | def get_identifier(cls, instance):
    method save_model (line 80) | def save_model(self, request, obj, form, change):

FILE: cmsplugin_cascade/generic/settings.py
  function set_defaults (line 3) | def set_defaults(config):

FILE: cmsplugin_cascade/generic/simple_wrapper.py
  class SimpleWrapperFormMixin (line 10) | class SimpleWrapperFormMixin(EntangledModelFormMixin):
    class Meta (line 20) | class Meta:
  class SimpleWrapperPlugin (line 24) | class SimpleWrapperPlugin(TransparentContainer, CascadePluginBase):
    method get_identifier (line 33) | def get_identifier(cls, instance):
    method get_render_template (line 40) | def get_render_template(self, context, instance, placeholder):

FILE: cmsplugin_cascade/generic/text_image.py
  class TextImageFormMixin (line 13) | class TextImageFormMixin(ImageFormMixin):
    class Meta (line 52) | class Meta:
  class TextImagePlugin (line 56) | class TextImagePlugin(LinkPluginBase):
    class Media (line 69) | class Media:
    method requires_parent_plugin (line 73) | def requires_parent_plugin(cls, slot, page):
    method get_inline_styles (line 81) | def get_inline_styles(cls, instance):
    method render (line 88) | def render(self, context, instance, placeholder):

FILE: cmsplugin_cascade/hide_plugins.py
  class HidePluginFormMixin (line 9) | class HidePluginFormMixin(EntangledModelFormMixin):
    class Meta (line 16) | class Meta:
  class HidePluginMixin (line 20) | class HidePluginMixin:
    method get_form (line 41) | def get_form(self, request, obj=None, **kwargs):
    method get_render_template (line 47) | def get_render_template(self, context, instance, placeholder):

FILE: cmsplugin_cascade/icon/admin.py
  class UploadIconsForms (line 13) | class UploadIconsForms(ModelForm):
    class Meta (line 14) | class Meta:
    method media (line 18) | def media(self):
    method clean (line 27) | def clean(self):
  class IconFontAdmin (line 61) | class IconFontAdmin(admin.ModelAdmin):
    method save_model (line 66) | def save_model(self, request, obj, form, change):
    method get_readonly_fields (line 78) | def get_readonly_fields(self, request, obj=None):
    method preview_icons (line 84) | def preview_icons(self, obj):
    method num_icons (line 93) | def num_icons(self, obj):
    method css_prefix (line 100) | def css_prefix(self, obj):

FILE: cmsplugin_cascade/icon/forms.py
  function get_default_icon_font (line 9) | def get_default_icon_font():
  class IconFormMixin (line 16) | class IconFormMixin(EntangledModelFormMixin):
    class Meta (line 28) | class Meta:
    method __init__ (line 31) | def __init__(self, *args, **kwargs):

FILE: cmsplugin_cascade/icon/plugin_base.py
  class IconPluginMixin (line 7) | class IconPluginMixin(CascadePluginMixinBase):
    class Media (line 11) | class Media:
    method changeform_view (line 15) | def changeform_view(self, request, object_id=None, form_url='', extra_...
    method render (line 19) | def render(self, context, instance, placeholder):

FILE: cmsplugin_cascade/icon/simpleicon.py
  class SimpleIconPlugin (line 10) | class SimpleIconPlugin(IconPluginMixin, LinkPluginBase):
    class Media (line 20) | class Media:

FILE: cmsplugin_cascade/icon/texticon.py
  class TextIconPlugin (line 10) | class TextIconPlugin(IconPluginMixin, LinkPluginBase):
    class Media (line 24) | class Media:
    method requires_parent_plugin (line 28) | def requires_parent_plugin(cls, slot, page):

FILE: cmsplugin_cascade/icon/utils.py
  function unzip_archive (line 12) | def unzip_archive(label, zip_ref):

FILE: cmsplugin_cascade/image.py
  class ImageFormMixin (line 9) | class ImageFormMixin(EntangledModelFormMixin):
    class Meta (line 26) | class Meta:
    method __init__ (line 29) | def __init__(self, *args, **kwargs):
    method clean_image_file (line 34) | def clean_image_file(self):
  class ImagePropertyMixin (line 46) | class ImagePropertyMixin:
    method __str__ (line 50) | def __str__(self):
    method image (line 57) | def image(self):
    method post_copy (line 62) | def post_copy(self, old_instance, new_old_ziplist):

FILE: cmsplugin_cascade/leaflet/map.py
  class MarkerModelMixin (line 29) | class MarkerModelMixin:
    method data (line 31) | def data(self):
  class MarkerForm (line 35) | class MarkerForm(CascadeModelForm):
    class Meta (line 86) | class Meta:
    method clean (line 90) | def clean(self):
  class MarkerInline (line 113) | class MarkerInline(StackedInline):
  class LeafletFormMixin (line 122) | class LeafletFormMixin(CascadeModelFormMixin):
    class Meta (line 155) | class Meta:
    method clean (line 159) | def clean(self):
  class LeafletModelMixin (line 171) | class LeafletModelMixin:
    method map_position (line 173) | def map_position(self):
  class LeafletPlugin (line 177) | class LeafletPlugin(WithInlineElementsMixin, CascadePluginBase):
    class Media (line 191) | class Media:
    method add_view (line 204) | def add_view(self, request, form_url='', extra_context=None):
    method change_view (line 208) | def change_view(self, request, object_id, form_url='', extra_context=N...
    method render (line 212) | def render(self, context, instance, placeholder):
    method get_css_classes (line 254) | def get_css_classes(cls, obj):
    method get_identifier (line 262) | def get_identifier(cls, obj):
    method get_data_representation (line 268) | def get_data_representation(cls, instance):

FILE: cmsplugin_cascade/leaflet/settings.py
  function set_defaults (line 5) | def set_defaults(config):

FILE: cmsplugin_cascade/link/cms_plugins.py
  class TextLinkPlugin (line 10) | class TextLinkPlugin(LinkPluginBase):
    class Media (line 19) | class Media:
    method get_identifier (line 23) | def get_identifier(cls, obj):
    method requires_parent_plugin (line 27) | def requires_parent_plugin(cls, slot, page):

FILE: cmsplugin_cascade/link/forms.py
  function format_page_link (line 25) | def format_page_link(title, path):
  class PageSelect2Widget (line 30) | class PageSelect2Widget(HeavySelect2Widget):
    method __init__ (line 31) | def __init__(self, *args, **kwargs):
    method media (line 36) | def media(self):
    method render (line 42) | def render(self, *args, **kwargs):
  class LinkSearchField (line 53) | class LinkSearchField(ModelChoiceField):
    method __init__ (line 56) | def __init__(self, *args, **kwargs):
    method clean (line 65) | def clean(self, value):
  class SectionChoiceField (line 70) | class SectionChoiceField(fields.ChoiceField):
    method __init__ (line 71) | def __init__(self, *args, **kwargs):
    method valid_value (line 75) | def valid_value(self, value):
  class LinkForm (line 83) | class LinkForm(EntangledModelFormMixin):
    class Meta (line 159) | class Meta:
    method __init__ (line 165) | def __init__(self, *args, **kwargs):
    method _preset_section (line 178) | def _preset_section(self, instance):
    method _post_clean (line 192) | def _post_clean(self):
    method clean_phone_number (line 218) | def clean_phone_number(self):
    method unset_required_for (line 222) | def unset_required_for(cls, sharable_fields):
  class TextLinkFormMixin (line 233) | class TextLinkFormMixin(EntangledModelFormMixin):
    class Meta (line 240) | class Meta:

FILE: cmsplugin_cascade/link/plugin_base.py
  class LinkPluginBase (line 10) | class LinkPluginBase(CascadePluginBase):
    class Media (line 18) | class Media:
    method get_link (line 23) | def get_link(cls, obj):
  class DefaultLinkPluginBase (line 55) | class DefaultLinkPluginBase(LinkPluginBase):
  class LinkElementMixin (line 62) | class LinkElementMixin:
    method __str__ (line 68) | def __str__(self):
    method link (line 72) | def link(self):
    method content (line 76) | def content(self):
    method download_name (line 80) | def download_name(self):

FILE: cmsplugin_cascade/migrations/0001_initial.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0002_auto_20150530_1018.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0003_inlinecascadeelement.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0004_auto_20151112_0147.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0005_tabset_and_clipboard.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0006_bootstrapgallerypluginmodel.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0007_add_proxy_models.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0008_sortableinlinecascadeelement.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0009_cascadepage.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0010_refactor_heading.py
  function forwards (line 3) | def forwards(apps, schema_editor):
  function backwards (line 12) | def backwards(apps, schema_editor):
  class Migration (line 21) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0011_merge_sharable_with_cascadeelement.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0012_auto_20160619_1854.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0013_iconfont.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0014_glossary_field.py
  function forwards (line 52) | def forwards(apps, schema_editor):
  function backwards (line 59) | def backwards(apps, schema_editor):
  function migrate_glossary (line 66) | def migrate_glossary(apps, field_mappings):
  class Migration (line 80) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0015_carousel_slide.py
  function _replace_text_body (line 11) | def _replace_text_body(old_body, input_pattern, output_tag, id_format):
  function forwards (line 40) | def forwards(apps, schema_editor):
  function backwards (line 72) | def backwards(apps, schema_editor):
  class Migration (line 76) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0016_shared_glossary_uneditable.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0017_fake_proxy_models.py
  class Migration (line 7) | class Migration(migrations.Migration):
    method operations (line 18) | def operations(self):

FILE: cmsplugin_cascade/migrations/0018_iconfont_color.py
  function forwards (line 5) | def forwards(apps, schema_editor):
  function backwards (line 22) | def backwards(apps, schema_editor):
  class Migration (line 39) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0019_verbose_table_names.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0020_page_icon_font.py
  function forwards (line 5) | def forwards(apps, schema_editor):
  function backwards (line 9) | def backwards(apps, schema_editor):
  class Migration (line 13) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0021_cascadepage_verbose_name.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0022_auto_20181202_1055.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0023_iconfont_is_default.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0024_page_icon_font.py
  function forwards (line 6) | def forwards(apps, schema_editor):
  function backwards (line 22) | def backwards(apps, schema_editor):
  class Migration (line 26) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0025_texteditorconfigfields.py
  class Migration (line 4) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0026_cascadepage_menu_symbol.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0027_version_1.py
  function migrate_link (line 5) | def migrate_link(glossary):
  function migrate_icon (line 43) | def migrate_icon(glossary):
  function migrate_image (line 52) | def migrate_image(glossary):
  function forwards (line 66) | def forwards(apps, schema_editor):
  function backwards (line 84) | def backwards(apps, schema_editor):
  class Migration (line 88) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0028_cascade_clipboard.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0029_json_field.py
  function backwards (line 6) | def backwards(apps, schema_editor):
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0030_separate_ids_per_language.py
  function forwards (line 5) | def forwards(apps, schema_editor):
  function backwards (line 17) | def backwards(apps, schema_editor):
  class Migration (line 29) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/migrations/0031_alter_texteditorconfigfields_element_type.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cmsplugin_cascade/mixins.py
  class CascadePluginMixin (line 4) | class CascadePluginMixin:
    method get_tag_type (line 11) | def get_tag_type(self, instance):
    method get_css_classes (line 18) | def get_css_classes(cls, instance):
    method get_inline_styles (line 35) | def get_inline_styles(cls, instance):
    method get_html_tag_attributes (line 46) | def get_html_tag_attributes(cls, instance):
  class WithInlineElementsMixin (line 57) | class WithInlineElementsMixin:
    method get_data_representation (line 63) | def get_data_representation(cls, instance):
    method add_inline_elements (line 69) | def add_inline_elements(cls, instance, inlines):
  class WithSortableInlineElementsMixin (line 75) | class WithSortableInlineElementsMixin:
    method get_data_representation (line 81) | def get_data_representation(cls, instance):
    method add_inline_elements (line 87) | def add_inline_elements(cls, instance, inlines):

FILE: cmsplugin_cascade/models.py
  class SharedGlossary (line 23) | class SharedGlossary(models.Model):
    class Meta (line 46) | class Meta:
    method __str__ (line 50) | def __str__(self):
    method save (line 53) | def save(self, force_insert=False, force_update=False, using=None, upd...
  class CascadeElement (line 64) | class CascadeElement(CascadeModelBase):
    class Meta (line 75) | class Meta:
    method copy_relations (line 80) | def copy_relations(self, oldinstance):
  class SharableCascadeElement (line 92) | class SharableCascadeElement(CascadeElement):
    class Meta (line 96) | class Meta:
    method __getattribute__ (line 99) | def __getattribute__(self, name):
  class InlineCascadeElement (line 109) | class InlineCascadeElement(models.Model):
    class Meta (line 121) | class Meta:
  class SortableInlineCascadeElement (line 125) | class SortableInlineCascadeElement(models.Model):
    class Meta (line 142) | class Meta:
    method __str__ (line 146) | def __str__(self):
  class PluginExtraFields (line 150) | class PluginExtraFields(models.Model):
    class Meta (line 181) | class Meta:
    method __str__ (line 185) | def __str__(self):
    method name (line 189) | def name(self):
  class TextEditorConfigFields (line 193) | class TextEditorConfigFields(models.Model):
    class Meta (line 215) | class Meta:
    method get_config (line 218) | def get_config(self):
  class Segmentation (line 227) | class Segmentation(models.Model):
    class Meta (line 228) | class Meta:
  class CascadeClipboard (line 234) | class CascadeClipboard(models.Model):
    class Meta (line 271) | class Meta:
    method __str__ (line 275) | def __str__(self):
  class FilePathField (line 279) | class FilePathField(models.FilePathField):
    method __init__ (line 284) | def __init__(self, **kwargs):
    method deconstruct (line 288) | def deconstruct(self):
  class IconFont (line 294) | class IconFont(models.Model):
    class Meta (line 320) | class Meta:
    method __str__ (line 324) | def __str__(self):
    method get_icon_families (line 327) | def get_icon_families(self):
    method get_stylesheet_url (line 340) | def get_stylesheet_url(self):
    method config_data_as_json (line 346) | def config_data_as_json(self):
    method delete_icon_font (line 353) | def delete_icon_font(cls, instance=None, **kwargs):
  class CascadePage (line 366) | class CascadePage(PageExtension):
    class Meta (line 398) | class Meta:
    method __str__ (line 402) | def __str__(self):
    method assure_relation (line 406) | def assure_relation(cls, cms_page):
    method delete_cascade_element (line 416) | def delete_cascade_element(cls, instance=None, **kwargs):

FILE: cmsplugin_cascade/models_base.py
  class CascadeModelBase (line 11) | class CascadeModelBase(CMSPlugin):
    class Meta (line 15) | class Meta:
    method __str__ (line 27) | def __str__(self):
    method plugin_class (line 31) | def plugin_class(self):
    method tag_type (line 35) | def tag_type(self):
    method css_classes (line 39) | def css_classes(self):
    method inline_styles (line 44) | def inline_styles(self):
    method html_tag_attributes (line 49) | def html_tag_attributes(self):
    method get_parent_instance (line 56) | def get_parent_instance(self):
    method get_parent_glossary (line 72) | def get_parent_glossary(self):
    method get_complete_glossary (line 84) | def get_complete_glossary(self):
    method get_num_children (line 95) | def get_num_children(self):
    method sanitize_children (line 101) | def sanitize_children(self):
    method from_db (line 114) | def from_db(cls, db, field_names, values):
    method save (line 120) | def save(self, sanitize_only=False, *args, **kwargs):
    method _get_cascade_elements (line 134) | def _get_cascade_elements(cls):

FILE: cmsplugin_cascade/plugin_base.py
  function create_proxy_model (line 27) | def create_proxy_model(name, model_mixins, base_model, attrs=None, modul...
  class CascadePluginMixinMetaclass (line 48) | class CascadePluginMixinMetaclass(MediaDefiningClass):
    method __new__ (line 51) | def __new__(cls, name, bases, attrs):
  class CascadePluginMixinBase (line 66) | class CascadePluginMixinBase(metaclass=CascadePluginMixinMetaclass):
  class CascadePluginBaseMetaclass (line 72) | class CascadePluginBaseMetaclass(CascadePluginMixinMetaclass, CMSPluginB...
    method __new__ (line 86) | def __new__(cls, name, bases, attrs):
  class TransparentWrapper (line 133) | class TransparentWrapper:
    method get_child_classes (line 147) | def get_child_classes(cls, slot, page, instance=None):
    method get_parent_classes (line 161) | def get_parent_classes(cls, slot, page, instance=None):
  class TransparentContainer (line 172) | class TransparentContainer(TransparentWrapper):
    method get_plugins (line 183) | def get_plugins():
  class CascadeFormMixin (line 197) | class CascadeFormMixin(EntangledModelFormMixin):
    class Meta (line 198) | class Meta:
  class CascadePluginBase (line 202) | class CascadePluginBase(metaclass=CascadePluginBaseMetaclass):
    class Media (line 209) | class Media:
    method __init__ (line 213) | def __init__(self, model=None, admin_site=None):
    method __repr__ (line 216) | def __repr__(self):
    method super (line 220) | def super(cls, klass, instance):
    method _get_parent_classes_transparent (line 230) | def _get_parent_classes_transparent(cls, slot, page, instance=None):
    method get_child_classes (line 246) | def get_child_classes(cls, slot, page, instance=None):
    method get_parent_classes (line 266) | def get_parent_classes(cls, slot, page, instance=None):
    method get_identifier (line 270) | def get_identifier(cls, instance):
    method sanitize_model (line 277) | def sanitize_model(cls, instance):
    method get_data_representation (line 290) | def get_data_representation(cls, instance):
    method add_inline_elements (line 297) | def add_inline_elements(cls, instance, inlines):
    method add_shared_reference (line 303) | def add_shared_reference(cls, instance, shared_glossary):
    method extend_children (line 308) | def extend_children(self, parent, wanted_children, child_class, child_...
    method get_form (line 321) | def get_form(self, request, obj=None, **kwargs):
    method get_parent_instance (line 332) | def get_parent_instance(self, request=None, obj=None):
    method get_previous_instance (line 363) | def get_previous_instance(self, obj):
    method get_next_instance (line 374) | def get_next_instance(self, obj):
    method render_change_form (line 385) | def render_change_form(self, request, context, add=False, change=False...
    method in_edit_mode (line 402) | def in_edit_mode(self, request, placeholder):

FILE: cmsplugin_cascade/render_template.py
  class RenderTemplateFormMixin (line 9) | class RenderTemplateFormMixin(EntangledModelFormMixin):
    class Meta (line 16) | class Meta:
  class RenderTemplateMixin (line 20) | class RenderTemplateMixin(metaclass=MediaDefiningClass):
    method get_template_choices (line 27) | def get_template_choices(cls):
    method get_form (line 30) | def get_form(self, request, obj=None, **kwargs):
    method get_render_template (line 40) | def get_render_template(self, context, instance, placeholder):

FILE: cmsplugin_cascade/segmentation/admin.py
  class SegmentationAdminMetaclass (line 8) | class SegmentationAdminMetaclass(MediaDefiningClass):
    method __new__ (line 9) | def __new__(cls, name, bases, attrs):
  class SegmentationAdmin (line 15) | class SegmentationAdmin(admin.ModelAdmin, metaclass=SegmentationAdminMet...
    class Media (line 16) | class Media:
    method get_model_perms (line 19) | def get_model_perms(self, request):
    method get_queryset (line 25) | def get_queryset(self, request):

FILE: cmsplugin_cascade/segmentation/cms_plugins.py
  class Template (line 13) | class Template(DjangoTemplate):
    method render (line 14) | def render(self, context):
  class SegmentFormMixin (line 20) | class SegmentFormMixin(EntangledModelFormMixin):
    class Meta (line 32) | class Meta:
    method __init__ (line 35) | def __init__(self, *args, **kwargs):
    method clean (line 40) | def clean(self):
  class SegmentPlugin (line 55) | class SegmentPlugin(TransparentContainer, CascadePluginBase):
    class Media (line 74) | class Media:
    method get_identifier (line 78) | def get_identifier(cls, obj):
    method get_render_template (line 84) | def get_render_template(self, context, instance, placeholder):
    method render (line 137) | def render(self, context, instance, placeholder):
    method get_form (line 144) | def get_form(self, request, obj=None, **kwargs):
    method save_model (line 159) | def save_model(self, request, obj, form, change):
    method _get_previous_open_tag (line 166) | def _get_previous_open_tag(self, obj):

FILE: cmsplugin_cascade/segmentation/cms_toolbars.py
  class SegmentationToolbar (line 9) | class SegmentationToolbar(CMSToolbar):
    method populate (line 10) | def populate(self):

FILE: cmsplugin_cascade/segmentation/mixins.py
  class SegmentPluginModelMixin (line 12) | class SegmentPluginModelMixin:
    method get_context_override (line 18) | def get_context_override(self, request):
    method render_plugin (line 26) | def render_plugin(self, context=None, placeholder=None, admin=False, p...
  class EmulateUserModelMixin (line 33) | class EmulateUserModelMixin(SegmentPluginModelMixin):
    method get_context_override (line 36) | def get_context_override(self, request):
  class EmulateUserAdminMixin (line 50) | class EmulateUserAdminMixin:
    method populate_toolbar (line 54) | def populate_toolbar(segmentation_menu, request):
    method get_urls (line 74) | def get_urls(self):
    method emulate_user (line 81) | def emulate_user(self, request, user_id):
    method emulate_users (line 88) | def emulate_users(self, request):
    method clear_emulations (line 162) | def clear_emulations(self, request):

FILE: cmsplugin_cascade/sharable/admin.py
  class SharedGlossaryAdmin (line 12) | class SharedGlossaryAdmin(admin.ModelAdmin):
    class Media (line 18) | class Media:
    method get_form (line 21) | def get_form(self, request, obj=None, **kwargs):
    method has_add_permission (line 40) | def has_add_permission(self, request):
    method add_view (line 44) | def add_view(self, request, form_url='', extra_context=None):
    method change_view (line 47) | def change_view(self, request, object_id, form_url='', extra_context=N...
    method used_by (line 55) | def used_by(self, obj):
    method render_change_form (line 62) | def render_change_form(self, request, context, add=False, change=False...
    method plugin_name (line 74) | def plugin_name(self, obj):

FILE: cmsplugin_cascade/sharable/fields.py
  class SharedSettingsWidget (line 7) | class SharedSettingsWidget(widgets.MultiWidget):
    class Media (line 8) | class Media:
    method __init__ (line 11) | def __init__(self):
    method decompress (line 18) | def decompress(self, value):
  class SharedSettingsField (line 22) | class SharedSettingsField(MultiValueField):
    method __init__ (line 23) | def __init__(self, *args, **kwargs):
    method clean (line 33) | def clean(self, value):

FILE: cmsplugin_cascade/sharable/forms.py
  class SelectSharedGlossary (line 14) | class SelectSharedGlossary(forms.Select):
    method create_option (line 17) | def create_option(self, name, value, label, selected, index, subindex=...
    method _get_data_glossary (line 24) | def _get_data_glossary(self, option_value):
    method _enrich_link (line 33) | def _enrich_link(self, glossary):
  class SharedFormMixin (line 45) | class SharedFormMixin(EntangledModelFormMixin):
    class Meta (line 60) | class Meta:
  class SharableFormMixin (line 64) | class SharableFormMixin(SharedFormMixin):
    class Meta (line 69) | class Meta:
    method clean_save_settings_as (line 72) | def clean_save_settings_as(self):
  class SharableGlossaryMixin (line 81) | class SharableGlossaryMixin(metaclass=forms.MediaDefiningClass):
    class Media (line 90) | class Media:
    method changeform_view (line 93) | def changeform_view(self, request, object_id=None, form_url='', extra_...
    method get_form (line 133) | def get_form(self, request, obj=None, **kwargs):
    method save_model (line 152) | def save_model(self, request, obj, form, change):
    method get_data_representation (line 172) | def get_data_representation(cls, instance):
    method add_shared_reference (line 181) | def add_shared_reference(cls, instance, shared_glossary_identifier):

FILE: cmsplugin_cascade/sphinx/cms_apps.py
  class SphinxDocsView (line 17) | class SphinxDocsView(TemplateView):
    method get (line 18) | def get(self, request, *args, **kwargs):
    method get_template_names (line 30) | def get_template_names(self):
    method get_context_data (line 33) | def get_context_data(self, page='index.html', **kwargs):
  class SphinxDocsApp (line 44) | class SphinxDocsApp(CMSApp):
    method get_urls (line 47) | def get_urls(self, page=None, language=None, **kwargs):

FILE: cmsplugin_cascade/sphinx/cms_menus.py
  class DocumentationMenu (line 12) | class DocumentationMenu(CMSAttachMenu):
    method get_nodes (line 15) | def get_nodes(self, request, root_page):

FILE: cmsplugin_cascade/sphinx/fragmentsbuilder.py
  class FragmentsBuilder (line 7) | class FragmentsBuilder(DirectoryHTMLBuilder):
    method __init__ (line 10) | def __init__(self, app):
    method prepare_writing (line 24) | def prepare_writing(self, docnames):
    method _get_local_toctree (line 42) | def _get_local_toctree(self, docname, collapse=True, **kwds):
    method finish (line 49) | def finish(self):
  function setup (line 55) | def setup(app):

FILE: cmsplugin_cascade/sphinx/link_plugin.py
  class DocumentationSelect2Widget (line 13) | class DocumentationSelect2Widget(Select2Widget):
    method render (line 14) | def render(self, name, value, attrs=None, renderer=None):
  function get_documents_map (line 19) | def get_documents_map():
  class SphinxDocsLinkForm (line 35) | class SphinxDocsLinkForm(LinkForm):
    method clean_documentation (line 51) | def clean_documentation(self):
    method set_initial_documentation (line 58) | def set_initial_documentation(self, initial):
  class SphinxDocsLinkPlugin (line 65) | class SphinxDocsLinkPlugin(LinkPluginBase):
    class Media (line 72) | class Media:
    method get_link (line 76) | def get_link(cls, obj):

FILE: cmsplugin_cascade/static/cascade/js/admin/cascadepage.js
  function fontChanged (line 13) | function fontChanged() {
  function selectIcon (line 31) | function selectIcon() {
  function renderIcons (line 37) | function renderIcons(response) {

FILE: cmsplugin_cascade/static/cascade/js/admin/clipboard.js
  function o (line 7) | function o(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&re...
  function r (line 7) | function r(t,e){if(i)return i.call(t,e);for(var n=t.parentNode.querySele...
  function r (line 7) | function r(t,e,n,r){var i=o.apply(this,arguments);return t.addEventListe...
  function o (line 7) | function o(t,e,n,r){return function(n){n.delegateTarget=i(n.target,e,!0)...
  function r (line 7) | function r(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required argume...
  function o (line 7) | function o(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.r...
  function i (line 7) | function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.ad...
  function a (line 7) | function a(t,e,n){return s(document.body,t,e,n)}
  function r (line 7) | function r(t){var e;if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName)t.f...
  function r (line 7) | function r(){}
  function r (line 7) | function r(){o.off(t,r),e.apply(n,arguments)}
  function r (line 7) | function r(t){return t&&t.__esModule?t:{"default":t}}
  function o (line 7) | function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a ...
  function t (line 7) | function t(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.en...
  function t (line 7) | function t(e){o(this,t),this.resolveOptions(e),this.initSelection()}
  function r (line 7) | function r(t){return t&&t.__esModule?t:{"default":t}}
  function o (line 7) | function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a ...
  function i (line 7) | function i(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("S...
  function a (line 7) | function a(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e....
  function e (line 7) | function e(n,r){o(this,e),t.call(this),this.resolveOptions(r),this.liste...

FILE: cmsplugin_cascade/static/cascade/js/admin/colorpicker.js
  function checkboxChanged (line 5) | function checkboxChanged(checkboxInput) {

FILE: cmsplugin_cascade/static/cascade/js/admin/segmentation.js
  function reloadBrowser (line 5) | function reloadBrowser() {

FILE: cmsplugin_cascade/static/cascade/js/admin/sharedsettingsfield.js
  function saveAsChanged (line 5) | function saveAsChanged(checkboxInput) {

FILE: cmsplugin_cascade/static/cascade/js/picturefill.js
  function picturefill (line 500) | function picturefill( options ) {
  function runPicturefill (line 593) | function runPicturefill() {

FILE: cmsplugin_cascade/static/cascade/js/ring.js
  function declare (line 40) | function declare(_) {

FILE: cmsplugin_cascade/strides.py
  class EmulateQuerySet (line 18) | class EmulateQuerySet:
    method __init__ (line 19) | def __init__(self, elements):
    method all (line 22) | def all(self):
  class StrideElementBase (line 27) | class StrideElementBase:
    method __init__ (line 31) | def __init__(self, plugin, data, children_data, parent=None):
    method pk (line 40) | def pk(self):
    method plugin_class (line 44) | def plugin_class(self):
    method child_plugin_instances (line 47) | def child_plugin_instances(self):
    method get_num_children (line 54) | def get_num_children(self):
    method get_complete_glossary (line 57) | def get_complete_glossary(self):
    method get_parent_glossary (line 63) | def get_parent_glossary(self):
    method tag_type (line 72) | def tag_type(self):
    method css_classes (line 76) | def css_classes(self):
    method inline_styles (line 81) | def inline_styles(self):
    method html_tag_attributes (line 86) | def html_tag_attributes(self):
  class TextStrideElement (line 94) | class TextStrideElement:
    method __init__ (line 95) | def __init__(self, plugin, data, children_data, parent=None):
    method tags_to_user_html (line 102) | def tags_to_user_html(self, context, placeholder):
  class StridePluginBase (line 122) | class StridePluginBase(CascadePluginMixin):
    method __init__ (line 128) | def __init__(self, model=None, admin_site=None, glossary_fields=None):
    method super (line 135) | def super(cls, klass, instance):
    method render (line 138) | def render(self, context, instance, placeholder):
    method _get_render_template (line 144) | def _get_render_template(self, context, instance, placeholder):
    method in_edit_mode (line 156) | def in_edit_mode(self, request, placeholder):
    method get_previous_instance (line 159) | def get_previous_instance(self, obj):
    method get_next_instance (line 166) | def get_next_instance(self, obj):
  class TextStridePlugin (line 174) | class TextStridePlugin(StridePluginBase):
    method render (line 177) | def render(self, context, instance, placeholder):
  class StrideContentRenderer (line 184) | class StrideContentRenderer:
    method __init__ (line 185) | def __init__(self, request):
    method render_cascade (line 190) | def render_cascade(self, context, tree_data):
    method render_plugin (line 204) | def render_plugin(self, instance, context, placeholder=None, editable=...
    method user_is_on_edit_mode (line 230) | def user_is_on_edit_mode(self):
    method get_cached_template (line 233) | def get_cached_template(self, template):
  function register_stride (line 242) | def register_stride(name, bases, attrs, model_mixins):

FILE: cmsplugin_cascade/templatetags/cascade_tags.py
  class StrideRenderer (line 21) | class StrideRenderer(Tag):
    method render_tag (line 35) | def render_tag(self, context, datafile):
  class RenderPlugin (line 67) | class RenderPlugin(Tag):
    method render_tag (line 77) | def render_tag(self, context, plugin):
  function is_valid_image (line 104) | def is_valid_image(image):
  function sphinx_docs_include (line 112) | def sphinx_docs_include(path):

FILE: cmsplugin_cascade/utils.py
  function remove_duplicates (line 9) | def remove_duplicates(lst):
  function rectify_partial_form_field (line 17) | def rectify_partial_form_field(base_field, partial_form_fields):
  function validate_link (line 31) | def validate_link(link_data):
  function compute_aspect_ratio (line 44) | def compute_aspect_ratio(image):
  function compute_aspect_ratio_with_glossary (line 52) | def compute_aspect_ratio_with_glossary(glossary):
  function get_image_size (line 60) | def get_image_size(width, image_height, aspect_ratio):
  function parse_responsive_length (line 72) | def parse_responsive_length(responsive_length):
  class CascadeUtilitiesMixin (line 87) | class CascadeUtilitiesMixin(metaclass=MediaDefiningClass):
    method __str__ (line 93) | def __str__(self):
    method get_form (line 96) | def get_form(self, request, obj=None, **kwargs):
    method get_css_classes (line 103) | def get_css_classes(cls, obj):
  class NamedCSSClasses (line 116) | class NamedCSSClasses:
    method __new__ (line 137) | def __new__(cls, choices):

FILE: cmsplugin_cascade/widgets.py
  class JSONMultiWidget (line 13) | class JSONMultiWidget(widgets.MultiWidget):
    method __init__ (line 20) | def __init__(self, glossary_fields):
    method decompress (line 38) | def decompress(self, values):
    method value_from_datadict (line 49) | def value_from_datadict(self, data, files, name):
    method value_omitted_from_data (line 60) | def value_omitted_from_data(self, data, files, name):
    method render (line 67) | def render(self, name, value, attrs=None, renderer=None):
  class NumberInputWidget (line 93) | class NumberInputWidget(widgets.NumberInput):
    method validate (line 98) | def validate(self, value):
  class AColorPickerMixin (line 103) | class AColorPickerMixin:
    method __init__ (line 106) | def __init__(self, with_alpha, *args, **kwargs):
    method media (line 116) | def media(self):
    method rgb2hex (line 124) | def rgb2hex(cls, val):
  class ColorPickerWidget (line 131) | class ColorPickerWidget(AColorPickerMixin, widgets.MultiWidget):
    method __init__ (line 138) | def __init__(self, with_alpha):
    class Media (line 146) | class Media:
    method decompress (line 149) | def decompress(self, values):
    method value_from_datadict (line 154) | def value_from_datadict(self, data, files, name):
    method get_context (line 165) | def get_context(self, name, value, attrs):
  class MultipleTextInputWidget (line 177) | class MultipleTextInputWidget(widgets.MultiWidget):
    method __init__ (line 183) | def __init__(self, labels, attrs=None):
    method decompress (line 188) | def decompress(self, values):
    method value_from_datadict (line 192) | def value_from_datadict(self, data, files, name):
    method render (line 196) | def render(self, name, value, attrs=None, renderer=None):
  class BorderChoiceWidget (line 211) | class BorderChoiceWidget(AColorPickerMixin, widgets.MultiWidget):
    method __init__ (line 217) | def __init__(self, choices, with_alpha):
    method media (line 227) | def media(self):
    method decompress (line 231) | def decompress(self, values):
    method value_from_datadict (line 235) | def value_from_datadict(self, data, files, name):
    method get_context (line 250) | def get_context(self, name, value, attrs):

FILE: docs/source/_static/shop_link_plugin.py
  class ProductSearchField (line 13) | class ProductSearchField(AutoModelSelect2Field):
    method security_check (line 18) | def security_check(self, request, *args, **kwargs):
    method prepare_value (line 24) | def prepare_value(self, value):
  class LinkForm (line 30) | class LinkForm(TextLinkForm):
    method clean_product (line 35) | def clean_product(self):
    method set_initial_product (line 43) | def set_initial_product(self, initial):
  class LinkPlugin (line 51) | class LinkPlugin(TextLinkPluginBase):
    class Media (line 56) | class Media:

FILE: examples/bs4demo/bs4demo/cms_plugins.py
  class Badge (line 12) | class Badge(CascadePluginBase):

FILE: examples/bs4demo/bs4demo/context_processors.py
  function cascade (line 5) | def cascade(request):

FILE: examples/bs4demo/bs4demo/urls.py
  class CascadeDemoView (line 8) | class CascadeDemoView(TemplateView):

FILE: examples/bs4demo/bs4demo/utils.py
  function find_django_migrations_module (line 2) | def find_django_migrations_module(module_name):

FILE: tests/bootstrap4/conftest.py
  function bootstrap_container (line 9) | def bootstrap_container(admin_site, cms_placeholder):
  function bootstrap_row (line 21) | def bootstrap_row(admin_site, bootstrap_container):
  function bootstrap_column (line 33) | def bootstrap_column(admin_site, bootstrap_row):

FILE: tests/bootstrap4/test_accordion.py
  function bootstrap_accordion (line 12) | def bootstrap_accordion(rf, admin_site, bootstrap_column):
  function test_edit_accordion_group (line 34) | def test_edit_accordion_group(rf, admin_site, bootstrap_accordion):

FILE: tests/bootstrap4/test_container.py
  function test_edit_bootstrap_container (line 11) | def test_edit_bootstrap_container(rf, bootstrap_container):
  function test_edit_bootstrap_row (line 32) | def test_edit_bootstrap_row(rf, bootstrap_row):

FILE: tests/bootstrap4/test_grid.py
  function test_breakpoint_iter (line 5) | def test_breakpoint_iter():
  function test_breakpoint_range (line 15) | def test_breakpoint_range():
  function test_breakpoint_partial (line 25) | def test_breakpoint_partial():
  function test_xs_cols (line 33) | def test_xs_cols():
  function test_fluid_xs_cols (line 56) | def test_fluid_xs_cols():
  function test_xs_cols_with_flex (line 79) | def test_xs_cols_with_flex():
  function test_xs_cols_with_auto_and_flex (line 107) | def test_xs_cols_with_auto_and_flex():
  function test_mix_flex_with_fixed (line 141) | def test_mix_flex_with_fixed():
  function test_mix_flex_with_auto (line 147) | def test_mix_flex_with_auto():
  function test_mix_fixed_with_auto (line 153) | def test_mix_fixed_with_auto():
  function test_growing_columns (line 159) | def test_growing_columns():
  function test_haricot (line 187) | def test_haricot():
  function test_nested_row (line 221) | def test_nested_row():
  function test_repr (line 255) | def test_repr():

FILE: tests/conftest.py
  function admin_site (line 12) | def admin_site():
  function cms_page (line 18) | def cms_page():
  function cms_placeholder (line 28) | def cms_placeholder(cms_page):
  class UserFactory (line 34) | class UserFactory(factory.django.DjangoModelFactory):
    class Meta (line 35) | class Meta:
    method create (line 39) | def create(cls, **kwargs):

FILE: tests/test_base.py
  class CascadeTestCase (line 14) | class CascadeTestCase(CMSTestCase):
    method setUp (line 17) | def setUp(self):
    method get_request_context (line 45) | def get_request_context(self):
    method get_html (line 57) | def get_html(self, model_instance, context):

FILE: tests/test_customplugin.py
  class CustomPlugin (line 7) | class CustomPlugin(CascadePluginBase):
  class CustomPluginTest (line 12) | class CustomPluginTest(TestCase):
    method test_register (line 14) | def test_register(self):
    method test_proxy_model_has_correct_app_label (line 17) | def test_proxy_model_has_correct_app_label(self):

FILE: tests/test_http.py
  class ContainerPluginTest (line 15) | class ContainerPluginTest(CMSTestCase):
    method setUp (line 16) | def setUp(self):
    method _create_and_configure_a_container_plugin (line 45) | def _create_and_configure_a_container_plugin(self):
    method test_without_reversion (line 74) | def test_without_reversion(self):
    method test_with_reversion (line 79) | def test_with_reversion(self):

FILE: tests/test_iconfont.py
  class IconFileFactory (line 21) | class IconFileFactory(factory.django.DjangoModelFactory):
    class Meta (line 22) | class Meta:
    method create (line 26) | def create(cls, **kwargs):
  function icon_font (line 40) | def icon_font(admin_client, icon_file_factory):
  function test_iconfont_change_view (line 66) | def test_iconfont_change_view(admin_client, icon_font):
  function simple_icon (line 83) | def simple_icon(admin_site, cms_placeholder, icon_font):
  function test_simple_icon (line 106) | def test_simple_icon(rf, simple_icon):

FILE: tests/test_missingmigrations.py
  function test_for_missing_migrations (line 10) | def test_for_missing_migrations():

FILE: tests/test_segmentation.py
  class SegmentationPluginTest (line 18) | class SegmentationPluginTest(CascadeTestCase):
    method setUp (line 19) | def setUp(self):
    method test_plugin_context (line 25) | def test_plugin_context(self):

FILE: tests/test_strides.py
  class StridePluginTest (line 22) | class StridePluginTest(CascadeTestCase):
    method setUp (line 23) | def setUp(self):
    method assertStyleEqual (line 28) | def assertStyleEqual(self, provided, expected):
    method skiptest_bootstrap_jumbotron (line 33) | def skiptest_bootstrap_jumbotron(self):
    method test_bootstrap_container (line 42) | def test_bootstrap_container(self):
    method skiptest_bootstrap_row (line 49) | def skiptest_bootstrap_row(self):
    method skiptest_bootstrap_column (line 55) | def skiptest_bootstrap_column(self):
    method skiptest_simple_wrapper (line 61) | def skiptest_simple_wrapper(self):
    method upload_icon_font (line 74) | def upload_icon_font(self):
    method test_framed_icon (line 98) | def test_framed_icon(self):
    method test_text_plugin (line 115) | def test_text_plugin(self):
    method test_carousel_plugin (line 123) | def test_carousel_plugin(self):
    method test_button_plugin (line 134) | def test_button_plugin(self):

FILE: tests/utils.py
  function get_request_context (line 4) | def get_request_context(request, extra_context=None):
Condensed preview — 316 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,147K chars).
[
  {
    "path": ".coveragerc",
    "chars": 68,
    "preview": "[run]\nbranch = True\n\n[report]\nshow_missing = True\nomit =\n    .tox/*\n"
  },
  {
    "path": ".editorconfig",
    "chars": 489,
    "preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = false\ntrim_trailing_whitespace = false\n\n# Set default charset\n["
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 990,
    "preview": "name: Publish djangocms-cascade\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  publish:\n    name: \"Publish release\"\n    run"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 1600,
    "preview": "name: Test djangocms-cascade\n\non:\n  push:\n    branches: [ \"master\", \"releases/*\" ]\n  pull_request:\n    branches: [ maste"
  },
  {
    "path": ".gitignore",
    "chars": 314,
    "preview": ".idea\n*.log\n*.pot\n*.pyc\n.project\n.pydevproject\n.settings\n.coverage\n.DS_Store\n.sass-cache\n.cache\n.pytest_cache\n*.db\n*.egg"
  },
  {
    "path": "BOOSTRAP-4.md",
    "chars": 2363,
    "preview": "# Migrating towards Bootstrap-4\n\nCurrently I'am refactoring **djangocms-cascade**, adding support for Bootstrap-4. This "
  },
  {
    "path": "LICENSE-MIT",
    "chars": 1077,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2013 Jacob Rief\n\nPermission is hereby granted, free of charge, to any person obtain"
  },
  {
    "path": "MANIFEST.in",
    "chars": 389,
    "preview": "include LICENSE-MIT\ninclude README.md\ninclude setup.py\nrecursive-include cmsplugin_cascade/locale *\nrecursive-include cm"
  },
  {
    "path": "README.md",
    "chars": 7253,
    "preview": "# djangocms-cascade\n\n[![Build Status](https://github.com/jrief/djangocms-cascade/actions/workflows/tests.yml/badge.svg)]"
  },
  {
    "path": "cmsplugin_cascade/__init__.py",
    "chars": 904,
    "preview": "\"\"\"\nSee PEP 386 (https://www.python.org/dev/peps/pep-0386/)\n\nRelease logic:\n 1. Remove \".devX\" from __version__ (below)\n"
  },
  {
    "path": "cmsplugin_cascade/admin.py",
    "chars": 6219,
    "preview": "from urllib.parse import urlparse\nimport requests\n\nfrom django.contrib import admin\nfrom django.contrib.sites.shortcuts "
  },
  {
    "path": "cmsplugin_cascade/app_settings.py",
    "chars": 7138,
    "preview": "\nclass AppSettings:\n\n    def _setting(self, name, default=None):\n        from django.conf import settings\n        return"
  },
  {
    "path": "cmsplugin_cascade/apps.py",
    "chars": 4609,
    "preview": "from django.apps import AppConfig, apps\nfrom django.conf import settings\nfrom django.core.exceptions import ImproperlyCo"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/accordion.py",
    "chars": 4206,
    "preview": "from django.forms import widgets, BooleanField, CharField\nfrom django.forms.fields import IntegerField\nfrom django.utils"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/buttons.py",
    "chars": 6615,
    "preview": "from django.forms import widgets\nfrom django.forms.fields import BooleanField, CharField, ChoiceField, MultipleChoiceFie"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/card.py",
    "chars": 2253,
    "preview": "from django.utils.translation import gettext_lazy as _\nfrom cms.plugin_pool import plugin_pool\nfrom cmsplugin_cascade.pl"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/carousel.py",
    "chars": 7463,
    "preview": "import re\nimport logging\nfrom django.forms import widgets\nfrom django.forms.fields import IntegerField, MultipleChoiceFi"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/container.py",
    "chars": 15868,
    "preview": "from django.core.exceptions import ValidationError\nfrom django.db.models import Q\nfrom django.forms import widgets\nfrom "
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/embeds.py",
    "chars": 4368,
    "preview": "import re\nfrom urllib.parse import urlparse, urlunparse, ParseResult\n\nfrom django.core.exceptions import ValidationError"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/fields.py",
    "chars": 504,
    "preview": "from cmsplugin_cascade.bootstrap4.grid import Breakpoint\nfrom cmsplugin_cascade.fields import MultiSizeField\n\n\nclass Boo"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/grid.py",
    "chars": 11616,
    "preview": "from copy import copy\nfrom enum import Enum, unique\nfrom functools import reduce\nimport itertools\nfrom operator import a"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/icon.py",
    "chars": 4370,
    "preview": "from django.forms import ChoiceField\nfrom django.utils.html import format_html, format_html_join\nfrom django.utils.safes"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/image.py",
    "chars": 5654,
    "preview": "import logging\nfrom django.forms import widgets, ChoiceField, MultipleChoiceField\nfrom django.utils.safestring import ma"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/jumbotron.py",
    "chars": 10609,
    "preview": "import logging\n\nfrom django.core.exceptions import ValidationError\nfrom django.forms import widgets, BooleanField, Choic"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/mixins.py",
    "chars": 7559,
    "preview": "from django.forms.fields import ChoiceField\nfrom django.utils.text import format_lazy\nfrom django.utils.translation impo"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/picture.py",
    "chars": 5193,
    "preview": "import logging\nfrom django.forms import widgets, MultipleChoiceField\nfrom django.utils.safestring import mark_safe\nfrom "
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/plugin_base.py",
    "chars": 1050,
    "preview": "import os\nfrom django.template.loader import get_template, TemplateDoesNotExist\nfrom cmsplugin_cascade import app_settin"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/secondary_menu.py",
    "chars": 2526,
    "preview": "from django.forms.fields import ChoiceField, IntegerField\nfrom django.utils.safestring import mark_safe\nfrom django.util"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/settings.py",
    "chars": 2829,
    "preview": "from collections import OrderedDict\nfrom django.conf import settings\nfrom django.utils.translation import gettext_lazy a"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/tabs.py",
    "chars": 3096,
    "preview": "from django.forms import widgets\nfrom django.forms.fields import BooleanField, CharField\nfrom django.utils.translation i"
  },
  {
    "path": "cmsplugin_cascade/bootstrap4/utils.py",
    "chars": 7432,
    "preview": "import logging\nfrom django.utils.translation import gettext_lazy as _\n\nfrom cmsplugin_cascade import app_settings\nfrom c"
  },
  {
    "path": "cmsplugin_cascade/clipboard/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/clipboard/admin.py",
    "chars": 3799,
    "preview": "from django.contrib import admin\nfrom django.contrib import messages\nfrom django.forms import widgets\nfrom django.forms."
  },
  {
    "path": "cmsplugin_cascade/clipboard/cms_plugins.py",
    "chars": 7449,
    "preview": "import json\n\nfrom django.contrib.admin import site as default_admin_site\nfrom django.contrib.admin.helpers import AdminF"
  },
  {
    "path": "cmsplugin_cascade/clipboard/cms_toolbars.py",
    "chars": 248,
    "preview": "from cms.toolbar_base import CMSToolbar\nfrom cms.toolbar_pool import toolbar_pool\n\n\n@toolbar_pool.register\nclass Cascade"
  },
  {
    "path": "cmsplugin_cascade/clipboard/forms.py",
    "chars": 896,
    "preview": "from django import forms\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django"
  },
  {
    "path": "cmsplugin_cascade/clipboard/utils.py",
    "chars": 4097,
    "preview": "from django.contrib.admin import site as default_admin_site\nfrom django.contrib import messages\nfrom django.utils.transl"
  },
  {
    "path": "cmsplugin_cascade/cms_plugins.py",
    "chars": 1088,
    "preview": "import sys\nfrom importlib import import_module\nfrom django.core.exceptions import ImproperlyConfigured\nfrom . import app"
  },
  {
    "path": "cmsplugin_cascade/cms_toolbars.py",
    "chars": 1067,
    "preview": "from django.conf import settings\nfrom django.utils.translation import gettext_lazy as _\nfrom cms.extensions.toolbar impo"
  },
  {
    "path": "cmsplugin_cascade/extra_fields/__init__.py",
    "chars": 24,
    "preview": "# -*- coding: utf-8 -*-\n"
  },
  {
    "path": "cmsplugin_cascade/extra_fields/admin.py",
    "chars": 7379,
    "preview": "import re\n\nfrom django.conf import settings\nfrom django.contrib import admin\nfrom django.core.exceptions import Validati"
  },
  {
    "path": "cmsplugin_cascade/extra_fields/config.py",
    "chars": 1353,
    "preview": "\nclass PluginExtraFieldsConfig:\n    \"\"\"\n    Each Cascade Plugin can be configured to accept extra fields, such as an ID "
  },
  {
    "path": "cmsplugin_cascade/extra_fields/mixins.py",
    "chars": 6703,
    "preview": "from django.contrib.sites.shortcuts import get_current_site\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom d"
  },
  {
    "path": "cmsplugin_cascade/fields.py",
    "chars": 11364,
    "preview": "import re\nimport json\nimport warnings\n\nfrom django.apps import apps\nfrom django.core.exceptions import ValidationError\nf"
  },
  {
    "path": "cmsplugin_cascade/forms.py",
    "chars": 1560,
    "preview": "from django.forms.formsets import DELETION_FIELD_NAME\nfrom django.forms.models import ModelForm\n\nfrom entangled.forms im"
  },
  {
    "path": "cmsplugin_cascade/generic/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/generic/custom_snippet.py",
    "chars": 1311,
    "preview": "from django.utils.html import format_html\nfrom django.utils.translation import gettext_lazy as _\nfrom cms.plugin_pool im"
  },
  {
    "path": "cmsplugin_cascade/generic/heading.py",
    "chars": 1653,
    "preview": "from django.forms import widgets, CharField, ChoiceField\nfrom django.utils.html import format_html\nfrom django.utils.saf"
  },
  {
    "path": "cmsplugin_cascade/generic/horizontal_rule.py",
    "chars": 413,
    "preview": "from django.utils.translation import gettext_lazy as _\n\nfrom cms.plugin_pool import plugin_pool\nfrom cmsplugin_cascade.p"
  },
  {
    "path": "cmsplugin_cascade/generic/mixins.py",
    "chars": 4411,
    "preview": "from django.core.exceptions import ValidationError, ObjectDoesNotExist\nfrom django.forms.fields import CharField\nfrom dj"
  },
  {
    "path": "cmsplugin_cascade/generic/settings.py",
    "chars": 653,
    "preview": "CASCADE_PLUGINS = ['custom_snippet', 'heading', 'horizontal_rule', 'simple_wrapper', 'text_image']\n\ndef set_defaults(con"
  },
  {
    "path": "cmsplugin_cascade/generic/simple_wrapper.py",
    "chars": 1595,
    "preview": "from django.forms import ChoiceField\nfrom django.utils.html import format_html\nfrom django.utils.translation import gett"
  },
  {
    "path": "cmsplugin_cascade/generic/text_image.py",
    "chars": 4716,
    "preview": "from django.forms import widgets, ChoiceField, MultipleChoiceField\nfrom django.utils.html import format_html_join\nfrom d"
  },
  {
    "path": "cmsplugin_cascade/hide_plugins.py",
    "chars": 2645,
    "preview": "from django.core.exceptions import ImproperlyConfigured\nfrom django.forms.fields import BooleanField\nfrom django.utils.t"
  },
  {
    "path": "cmsplugin_cascade/icon/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/icon/admin.py",
    "chars": 4672,
    "preview": "from django.contrib import admin\nfrom django.core.exceptions import ValidationError\nfrom django.forms import Media\nfrom "
  },
  {
    "path": "cmsplugin_cascade/icon/forms.py",
    "chars": 1127,
    "preview": "from django.forms import widgets, CharField, ModelChoiceField\nfrom django.utils.translation import gettext_lazy as _\n\nfr"
  },
  {
    "path": "cmsplugin_cascade/icon/plugin_base.py",
    "chars": 1324,
    "preview": "from django.utils.safestring import mark_safe\nfrom cmsplugin_cascade.models import IconFont\nfrom cmsplugin_cascade.plugi"
  },
  {
    "path": "cmsplugin_cascade/icon/settings.py",
    "chars": 45,
    "preview": "CASCADE_PLUGINS = ['simpleicon', 'texticon']\n"
  },
  {
    "path": "cmsplugin_cascade/icon/simpleicon.py",
    "chars": 875,
    "preview": "from django.utils.translation import gettext_lazy as _\n\nfrom cms.plugin_pool import plugin_pool\nfrom cmsplugin_cascade.l"
  },
  {
    "path": "cmsplugin_cascade/icon/texticon.py",
    "chars": 1076,
    "preview": "from django.utils.translation import gettext_lazy as _\n\nfrom cms.plugin_pool import plugin_pool\nfrom cmsplugin_cascade.l"
  },
  {
    "path": "cmsplugin_cascade/icon/utils.py",
    "chars": 1280,
    "preview": "import os, io, json, shutil\nfrom django.core.exceptions import SuspiciousFileOperation\nfrom cmsplugin_cascade import app"
  },
  {
    "path": "cmsplugin_cascade/image.py",
    "chars": 2329,
    "preview": "from django.forms.fields import CharField\nfrom django.utils.translation import gettext_lazy as _\n\nfrom entangled.forms i"
  },
  {
    "path": "cmsplugin_cascade/leaflet/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/leaflet/map.py",
    "chars": 10389,
    "preview": "import json\n\nfrom django.forms import widgets\nfrom django.forms.fields import CharField, BooleanField\nfrom django.db.mod"
  },
  {
    "path": "cmsplugin_cascade/leaflet/settings.py",
    "chars": 829,
    "preview": "from django.utils.safestring import mark_safe\n\nCASCADE_PLUGINS = ['map']\n\ndef set_defaults(config):\n    config.setdefaul"
  },
  {
    "path": "cmsplugin_cascade/link/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/link/cms_plugins.py",
    "chars": 1158,
    "preview": "from django.utils.safestring import mark_safe\nfrom django.utils.translation import gettext_lazy as _\n\nfrom cms.plugin_po"
  },
  {
    "path": "cmsplugin_cascade/link/config.py",
    "chars": 221,
    "preview": "from django.utils.module_loading import import_string\nfrom cmsplugin_cascade import app_settings\n\n\nLinkPluginBase, LinkF"
  },
  {
    "path": "cmsplugin_cascade/link/forms.py",
    "chars": 9132,
    "preview": "from django.core.exceptions import ObjectDoesNotExist, ValidationError\nfrom django.contrib.admin.sites import site as ad"
  },
  {
    "path": "cmsplugin_cascade/link/plugin_base.py",
    "chars": 3040,
    "preview": "from django.core.exceptions import ObjectDoesNotExist\nfrom django.utils.functional import cached_property\nfrom django.ut"
  },
  {
    "path": "cmsplugin_cascade/locale/de/LC_MESSAGES/django.po",
    "chars": 46843,
    "preview": "# djangocms-cascade\n# Copyright (C) 2021 Jacob Rief\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: PACKAGE VERSION\\n"
  },
  {
    "path": "cmsplugin_cascade/locale/fr/LC_MESSAGES/django.po",
    "chars": 36391,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cmsplugin_cascade/locale/it/LC_MESSAGES/django.po",
    "chars": 19715,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cmsplugin_cascade/locale/ru/LC_MESSAGES/django.po",
    "chars": 19789,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cmsplugin_cascade/locale/uk/LC_MESSAGES/django.po",
    "chars": 19789,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cmsplugin_cascade/migrations/0001_initial.py",
    "chars": 3828,
    "preview": "from django.db import models, migrations\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    "
  },
  {
    "path": "cmsplugin_cascade/migrations/0002_auto_20150530_1018.py",
    "chars": 1288,
    "preview": "from django.db import models, migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0003_inlinecascadeelement.py",
    "chars": 782,
    "preview": "from django.db import models, migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0004_auto_20151112_0147.py",
    "chars": 196,
    "preview": "from django.db import models, migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0005_tabset_and_clipboard.py",
    "chars": 777,
    "preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0006_bootstrapgallerypluginmodel.py",
    "chars": 196,
    "preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0007_add_proxy_models.py",
    "chars": 203,
    "preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0008_sortableinlinecascadeelement.py",
    "chars": 948,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    "
  },
  {
    "path": "cmsplugin_cascade/migrations/0009_cascadepage.py",
    "chars": 1290,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    "
  },
  {
    "path": "cmsplugin_cascade/migrations/0010_refactor_heading.py",
    "chars": 1062,
    "preview": "from django.db import migrations\n\ndef forwards(apps, schema_editor):\n    CascadeElement = apps.get_model('cmsplugin_casc"
  },
  {
    "path": "cmsplugin_cascade/migrations/0011_merge_sharable_with_cascadeelement.py",
    "chars": 2062,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\nfrom cmsplugin_cascade import app_settings\n\np"
  },
  {
    "path": "cmsplugin_cascade/migrations/0012_auto_20160619_1854.py",
    "chars": 523,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    "
  },
  {
    "path": "cmsplugin_cascade/migrations/0013_iconfont.py",
    "chars": 1286,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\nimport filer.fields.file\nimport cmsplugin_casc"
  },
  {
    "path": "cmsplugin_cascade/migrations/0014_glossary_field.py",
    "chars": 3051,
    "preview": "from django.db import migrations\n\nFIELD_MAPPINGS = {\n    'BootstrapButtonPlugin': [\n        ('button-type', 'button_type"
  },
  {
    "path": "cmsplugin_cascade/migrations/0015_carousel_slide.py",
    "chars": 2680,
    "preview": "import re\nimport warnings\nfrom html.parser import HTMLParser\nfrom django.db import migrations\nfrom cms.api import add_pl"
  },
  {
    "path": "cmsplugin_cascade/migrations/0016_shared_glossary_uneditable.py",
    "chars": 620,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    "
  },
  {
    "path": "cmsplugin_cascade/migrations/0017_fake_proxy_models.py",
    "chars": 1000,
    "preview": "from cmsplugin_cascade.models import CascadeModelBase\nfrom cmsplugin_cascade.plugin_base import fake_proxy_models\nfrom d"
  },
  {
    "path": "cmsplugin_cascade/migrations/0018_iconfont_color.py",
    "chars": 1522,
    "preview": "from django.db import migrations\nfrom cmsplugin_cascade.models import CascadeElement\n\n\ndef forwards(apps, schema_editor)"
  },
  {
    "path": "cmsplugin_cascade/migrations/0019_verbose_table_names.py",
    "chars": 1001,
    "preview": "from django.db import migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplugin_casc"
  },
  {
    "path": "cmsplugin_cascade/migrations/0020_page_icon_font.py",
    "chars": 776,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\ndef forwards(apps, schema_editor):\n    print"
  },
  {
    "path": "cmsplugin_cascade/migrations/0021_cascadepage_verbose_name.py",
    "chars": 374,
    "preview": "from django.db import migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplugin_casc"
  },
  {
    "path": "cmsplugin_cascade/migrations/0022_auto_20181202_1055.py",
    "chars": 558,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    "
  },
  {
    "path": "cmsplugin_cascade/migrations/0023_iconfont_is_default.py",
    "chars": 471,
    "preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0024_page_icon_font.py",
    "chars": 1358,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\nfrom cmsplugin_cascade.models import CascadeEl"
  },
  {
    "path": "cmsplugin_cascade/migrations/0025_texteditorconfigfields.py",
    "chars": 1064,
    "preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('cmsplu"
  },
  {
    "path": "cmsplugin_cascade/migrations/0026_cascadepage_menu_symbol.py",
    "chars": 784,
    "preview": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    "
  },
  {
    "path": "cmsplugin_cascade/migrations/0027_version_1.py",
    "chars": 3382,
    "preview": "from django.db import migrations\nfrom cmsplugin_cascade.models import CascadeElement\n\n\ndef migrate_link(glossary):\n    d"
  },
  {
    "path": "cmsplugin_cascade/migrations/0028_cascade_clipboard.py",
    "chars": 1135,
    "preview": "from django.conf import settings\nfrom django.db import migrations, models\nimport django.utils.timezone\n\n\nclass Migration"
  },
  {
    "path": "cmsplugin_cascade/migrations/0029_json_field.py",
    "chars": 2323,
    "preview": "# Generated by Django 3.1.5 on 2021-01-28 15:52\n\nfrom django.db import migrations, models\n\n\ndef backwards(apps, schema_e"
  },
  {
    "path": "cmsplugin_cascade/migrations/0030_separate_ids_per_language.py",
    "chars": 1050,
    "preview": "from django.db import migrations\nfrom django.conf import settings\n\n\ndef forwards(apps, schema_editor):\n    CascadePage ="
  },
  {
    "path": "cmsplugin_cascade/migrations/0031_alter_texteditorconfigfields_element_type.py",
    "chars": 680,
    "preview": "# Generated by Django 4.0.6.dev20220601124058 on 2022-08-02 08:54\n\nfrom django.db import migrations, models\n\n\nclass Migr"
  },
  {
    "path": "cmsplugin_cascade/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/mixins.py",
    "chars": 3548,
    "preview": "from cmsplugin_cascade.models import InlineCascadeElement, SortableInlineCascadeElement\n\n\nclass CascadePluginMixin:\n    "
  },
  {
    "path": "cmsplugin_cascade/models.py",
    "chars": 12117,
    "preview": "import json\nimport os\nimport shutil\nfrom collections import OrderedDict\nfrom urllib.parse import urljoin\nfrom pathlib im"
  },
  {
    "path": "cmsplugin_cascade/models_base.py",
    "chars": 5642,
    "preview": "from django.db import models\nfrom django.utils.html import mark_safe, format_html_join\nfrom django.utils.functional impo"
  },
  {
    "path": "cmsplugin_cascade/plugin_base.py",
    "chars": 18746,
    "preview": "from django.core.exceptions import ImproperlyConfigured\nfrom django.forms import MediaDefiningClass, ModelForm\nfrom djan"
  },
  {
    "path": "cmsplugin_cascade/render_template.py",
    "chars": 2051,
    "preview": "from django.forms import MediaDefiningClass\nfrom django.forms.fields import ChoiceField\nfrom django.utils.translation im"
  },
  {
    "path": "cmsplugin_cascade/segmentation/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/segmentation/admin.py",
    "chars": 1373,
    "preview": "from django.forms import MediaDefiningClass\nfrom django.contrib import admin\nfrom django.utils.module_loading import imp"
  },
  {
    "path": "cmsplugin_cascade/segmentation/cms_plugins.py",
    "chars": 8281,
    "preview": "import html\n\nfrom django.core.exceptions import ValidationError\nfrom django.forms.fields import CharField, ChoiceField\nf"
  },
  {
    "path": "cmsplugin_cascade/segmentation/cms_toolbars.py",
    "chars": 813,
    "preview": "from django.utils.module_loading import import_string\nfrom django.utils.translation import gettext_lazy as _\nfrom cms.to"
  },
  {
    "path": "cmsplugin_cascade/segmentation/mixins.py",
    "chars": 7088,
    "preview": "from django.contrib import admin\nfrom django.contrib.auth import get_user_model\nfrom django.http import HttpResponse, Ht"
  },
  {
    "path": "cmsplugin_cascade/sharable/__init__.py",
    "chars": 24,
    "preview": "# -*- coding: utf-8 -*-\n"
  },
  {
    "path": "cmsplugin_cascade/sharable/admin.py",
    "chars": 3685,
    "preview": "from django.contrib import admin\nfrom django.utils.translation import gettext as _\nfrom django.utils.html import format_"
  },
  {
    "path": "cmsplugin_cascade/sharable/fields.py",
    "chars": 1222,
    "preview": "from django.core.exceptions import ValidationError\nfrom django.forms import widgets\nfrom django.forms.fields import Char"
  },
  {
    "path": "cmsplugin_cascade/sharable/forms.py",
    "chars": 8553,
    "preview": "import json\nfrom copy import deepcopy\nfrom django import forms\nfrom django.contrib.admin.helpers import AdminForm\nfrom d"
  },
  {
    "path": "cmsplugin_cascade/sphinx/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cmsplugin_cascade/sphinx/cms_apps.py",
    "chars": 1950,
    "preview": "import io\nimport mimetypes\nimport os\nfrom django.conf import settings\nfrom django.core.exceptions import ViewDoesNotExis"
  },
  {
    "path": "cmsplugin_cascade/sphinx/cms_menus.py",
    "chars": 1282,
    "preview": "import io\nimport json\nimport os\nfrom django.conf import settings\nfrom django.urls import reverse_lazy\nfrom django.utils."
  },
  {
    "path": "cmsplugin_cascade/sphinx/fragmentsbuilder.py",
    "chars": 2072,
    "preview": "import json, os\nfrom docutils import nodes\nfrom sphinx.builders.dirhtml import DirectoryHTMLBuilder\nfrom sphinx.environm"
  },
  {
    "path": "cmsplugin_cascade/sphinx/link_plugin.py",
    "chars": 2601,
    "preview": "import io\nimport json\nimport os\nfrom django.conf import settings\nfrom django.forms import fields\nfrom django.utils.trans"
  },
  {
    "path": "cmsplugin_cascade/sphinx/static/cascade/sphinx/css/bootstrap-sphinx.css",
    "chars": 3440,
    "preview": "/*\n * bootstrap-sphinx.css\n * ~~~~~~~~~~~~~~~~~~~~\n *\n * Sphinx stylesheet -- Bootstrap theme.\n */\n\n/*\n * Imports to agg"
  },
  {
    "path": "cmsplugin_cascade/sphinx/static/cascade/sphinx/css/documentation.css",
    "chars": 10847,
    "preview": "/*\n * basic.css\n * ~~~~~~~~~\n *\n * Sphinx stylesheet -- basic theme.\n *\n * :copyright: Copyright 2007-2017 by the Sphinx"
  },
  {
    "path": "cmsplugin_cascade/sphinx/static/cascade/sphinx/js/link_plugin.js",
    "chars": 653,
    "preview": "\ndjango.jQuery(function($) {\n\t'use strict';\n\n\tdjango.cascade.SphinxDocsLinkPlugin = ring.create(eval(django.cascade.ring"
  },
  {
    "path": "cmsplugin_cascade/sphinx/theme/bootstrap-fragments/globaltoc.html",
    "chars": 62,
    "preview": "{{ toctree(maxdepth=0, collapse=True, includehidden=False) }}\n"
  },
  {
    "path": "cmsplugin_cascade/sphinx/theme/bootstrap-fragments/layout.html",
    "chars": 1083,
    "preview": "{% set bootstrap_version, navbar_version = \"3.3.7\", \"\" %}\n\n{%- set render_sidebar = (not embedded) and (not theme_noside"
  },
  {
    "path": "cmsplugin_cascade/sphinx/theme/bootstrap-fragments/theme.conf",
    "chars": 1642,
    "preview": "# Bootstrap Theme\n[theme]\ninherit = basic\nstylesheet = bootstrap-sphinx.css\npygments_style = tango\n\n# Configurable optio"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/LICENSE.md",
    "chars": 168,
    "preview": "fallback.svg is licensed according to the Creative Commons BY 4.0.\nIt is available from here: https://www.shareicon.net/"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/bootstrap4-buttons.css",
    "chars": 16442,
    "preview": ".form-row.field-button_type {\n  max-width: 630px;\n}\nform .field-button_type span.btn,\n  form .field-button_size span.btn"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/borderchoice.css",
    "chars": 161,
    "preview": ".cascade-border-choice em {\n  margin-left: 0.5em;\n  margin-right: 0.5em;\n}\n.cascade-border-choice .a-color-picker {\n  po"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/cascadepage.css",
    "chars": 637,
    "preview": ".field-menu_symbol h2 {\n\tmargin: 16px 0 8px;\n}\n.field-menu_symbol ul.font-family {\n\toverflow: hidden;\n\tline-height: 42px"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/clipboard.css",
    "chars": 1338,
    "preview": ".pull-left {\n\tfloat: left !important;\n}\n.clip-btn img {\n\t height: 20px;\n}\n.status-line {\n\theight: 30px;\n}\n.status-line s"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/colorpicker.css",
    "chars": 566,
    "preview": ".cascade-colorpicker .a-color-picker label {\n  width: initial;\n}\n.cascade-colorpicker input[type=\"color\"] {\n  padding: 0"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/editplugin.css",
    "chars": 1745,
    "preview": "form input:disabled, form textarea:disabled, form select:disabled { color: #888; border-style: dashed; }\nform .form-row."
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/iconfont.css",
    "chars": 328,
    "preview": ".preview-iconfont h2 {\n\tmargin: 16px 0 8px;\n}\n.preview-iconfont ul {\n\tline-height: 42px;\n}\n.preview-iconfont ul li {\n\tbo"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/iconplugin.css",
    "chars": 620,
    "preview": ".form-row.field-symbol h2 {\n\tmargin: 16px 0 8px;\n}\n.form-row.field-symbol ul.font-family {\n\toverflow: hidden;\n\tline-heig"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/leafletplugin.css",
    "chars": 991,
    "preview": "#leaflet_edit_map {\n\twidth: 100%;\n\theight: 500px;\n\tmargin-bottom: 20px;\n}\n.map-button {\n\tfont-size: 18px;\n\tline-height: "
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/linkplugin.css",
    "chars": 1226,
    "preview": "#id_ext_url {\n  background-position-x: calc(100% - 8px);\n  background-position-y: 50%;\n  background-clip: padding-box;\n "
  },
  {
    "path": "cmsplugin_cascade/static/cascade/css/admin/partialfields.css",
    "chars": 1693,
    "preview": "/*\nform .form-row { border-bottom: 1px solid #f3f3f3; padding: 8px 0; }\nform .form-row .form-row { border-bottom: none; "
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/buttonmixin.js",
    "chars": 1247,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\n\t// create class handling the client-side part of ButtonPlugin\n\tvar $glossar"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/buttonplugin.js",
    "chars": 149,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\n\tdjango.cascade.ButtonPlugin = ring.create(eval(django.cascade.ring_plugin_b"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/cascadepage.js",
    "chars": 2332,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\tvar $selectIconFont = $('#id_icon_font'),\n\t    $symbol = $('#id_menu_symbol'"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/clipboard.js",
    "chars": 9649,
    "preview": "/*!\n * clipboard.js v1.5.5\n * https://zenorocha.github.io/clipboard.js\n *\n * Licensed MIT © Zeno Rocha\n */\n!function(t){"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/colorpicker.js",
    "chars": 2177,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\tvar picker = null, AColorPicker = window.AColorPicker;\n\n\tfunction checkboxCh"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/framediconplugin.js",
    "chars": 979,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\n\tvar $glossary_icon_align = $('#id_glossary_text_align').find('input[name=\"t"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/iconplugin.js",
    "chars": 145,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\n\tdjango.cascade.IconPlugin = ring.create(eval(django.cascade.ring_plugin_bas"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/iconpluginmixin.js",
    "chars": 2990,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\n\t// create class handling the client-side part of plugins inheriting from Ic"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/imageplugin.js",
    "chars": 1339,
    "preview": "\ndjango.jQuery(function($) {\n\t'use strict';\n\n\tvar $image_responsive = $('#id_image_shapes_0');\n\n\t// create class handlin"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/jumbotronplugin.js",
    "chars": 2203,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\n\t// create class handling the client-side part of JumbotronPlugin\n\tvar base_"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/leafletplugin.js",
    "chars": 6073,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\n\tdjango.cascade.LeafletPlugin = ring.create(eval(django.cascade.ring_plugin_"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/linkplugin.js",
    "chars": 3836,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\tvar $document = $(document);\n\tvar $link_type = $(\"#id_link_type\"), $cmspage_"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/pictureplugin.js",
    "chars": 418,
    "preview": "\ndjango.jQuery(function($) {\n\t'use strict';\n\n\t// create class handling the client-side part of PicturePlugin\n\tdjango.cas"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/segmentation.js",
    "chars": 439,
    "preview": "\ndjango.jQuery(function($) {\n\t'use strict';\n\n\tfunction reloadBrowser() {\n\t\tvar parent = (window.parent) ? window.parent "
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/segmentplugin.js",
    "chars": 771,
    "preview": "\ndjango.jQuery(function($) {\n\t'use strict';\n\tvar $id_open_tag = $(\"#id_open_tag\");\n\tvar $condition_element = $(\".field-c"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/sharableglossary.js",
    "chars": 1215,
    "preview": "\ndjango.cascade = django.cascade || {};\n\ndjango.jQuery(function($) {\n\t'use strict';\n\n\tvar url = new URL(window.location."
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/sharedsettingsfield.js",
    "chars": 454,
    "preview": "django.jQuery(function($) {\n\t'use strict';\n\tvar $checkboxElem = $('.form-row.field-save_settings_as input[type=\"checkbox"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/textimageplugin.js",
    "chars": 409,
    "preview": "django.jQuery(function($) {\n\t// create class handling the client-side part of a TextImagePlugin\n\tdjango.cascade.TextImag"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/admin/textlinkplugin.js",
    "chars": 204,
    "preview": "django.jQuery(function($) {\n\t// create class handling the client-side part of a TextLinkPlugin\n\tdjango.cascade.TextLinkP"
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/picturefill.js",
    "chars": 19803,
    "preview": "/*! Picturefill - v2.1.0-beta - 2014-06-03\n* http://scottjehl.github.io/picturefill\n* Copyright (c) 2014 https://github."
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/ring.js",
    "chars": 15210,
    "preview": "/*\nRing.js\n\nCopyright (c) 2013, Nicolas Vanhoren\n\nReleased under the MIT license\n\nPermission is hereby granted, free of "
  },
  {
    "path": "cmsplugin_cascade/static/cascade/js/underscore.js",
    "chars": 43566,
    "preview": "//     Underscore.js 1.5.2\n//     http://underscorejs.org\n//     (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Invest"
  },
  {
    "path": "cmsplugin_cascade/strides.py",
    "chars": 10449,
    "preview": "from django.core.cache import caches\nfrom django.template.context import make_context\nfrom django.template.exceptions im"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/change_form.html",
    "chars": 1250,
    "preview": "{% extends \"admin/cms/usersettings/change_form.html\" %}\n{% load admin_urls i18n static %}\n\n{% block field_sets %}\n\t{{ pl"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/ckeditor.wysiwyg.txt",
    "chars": 427,
    "preview": "(function() {\n\tCKEDITOR.dtd.$removeEmpty.i = 0;\n\tCKEDITOR.stylesSet.add('default', [{% for text_editor_config in text_ed"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/clipboard_close_frame.html",
    "chars": 78,
    "preview": "<div>\n\t<div class=\"messagelist\">\n\t\t<div class=\"success\"></div>\n\t</div>\n</div>\n"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/clipboard_paste_plugins.html",
    "chars": 185,
    "preview": "<script>\n\twindow.top.CMS.API.StructureBoard.invalidateState('PASTE', {{ structure_data|safe }});\n</script>\n<div>\n\t<div c"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/clipboard_reload_page.html",
    "chars": 140,
    "preview": "<script>\n\twindow.top.CMS.API.Helpers.reloadBrowser();\n</script>\n<div>\n\t<div class=\"messagelist\">\n\t\t<div class=\"error\"></"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/leaflet_plugin_change_form.html",
    "chars": 318,
    "preview": "{% extends \"cascade/admin/change_form.html\" %}\n{% load i18n %}\n\n{% block after_field_sets %}\n\t{{ block.super }}\n\t<div id"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/legacy_widgets/button_sizes.html",
    "chars": 381,
    "preview": "{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widget=options.0 %}\n\t<"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/legacy_widgets/button_types.html",
    "chars": 328,
    "preview": "{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widget=options.0 %}\n\t<"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/legacy_widgets/container_breakpoints.html",
    "chars": 454,
    "preview": "{% load static %}{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widge"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/legacy_widgets/panel_types.html",
    "chars": 438,
    "preview": "{% load i18n %}{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widget="
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/segmentation_list.html",
    "chars": 1697,
    "preview": "{% extends \"admin/base_site.html\" %}\n<<<<<<< HEAD\n{% load i18n admin_urls admin_list static %}\n=======\n{% load i18n admi"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/sharedglossary_change_form.html",
    "chars": 745,
    "preview": "{% extends \"admin/change_form.html\" %}\n{% load cms_tags %}\n\n{% block after_field_sets %}\n\t{{ block.super }}\n\t<script typ"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/widgets/borderchoice.html",
    "chars": 460,
    "preview": "{% spaceless %}\n<div class=\"cascade-colorpicker cascade-border-choice\">\n\t{% with widget=widget.subwidgets.0 %}{% include"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/widgets/button_sizes.html",
    "chars": 339,
    "preview": "{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widget=options.0 %}\n\t<"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/widgets/button_types.html",
    "chars": 327,
    "preview": "{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widget=options.0 %}\n\t<"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/widgets/colorpicker.html",
    "chars": 403,
    "preview": "{% load i18n %}\n{% spaceless %}\n<div class=\"cascade-colorpicker cascade-color-picker\">\n\t{% with widget=widget.subwidgets"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/widgets/container_breakpoints.html",
    "chars": 427,
    "preview": "{% load static %}{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widge"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/widgets/inherit_color.html",
    "chars": 140,
    "preview": "{% load i18n %}\n<em style=\"margin-left: 1em; margin-right: 1em;\">{% trans \"Inherit\" %}</em> {% include \"django/forms/wid"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/admin/widgets/panel_types.html",
    "chars": 437,
    "preview": "{% load i18n %}{% spaceless %}\n<div class=\"form-row\">\n{% for group, options, index in widget.optgroups %}{% with widget="
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html",
    "chars": 997,
    "preview": "{% load l10n cascade_tags %}\n\n{% localize off %}{% spaceless %}{% with inline_styles=instance.inline_styles plugin_id=in"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/angular-ui/accordion.html",
    "chars": 1566,
    "preview": "{% load cascade_tags %}{% spaceless %}\n{% with inline_styles=instance.inline_styles %}\n\n<uib-accordion close-others=\"{% "
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/angular-ui/carousel.html",
    "chars": 471,
    "preview": "{% load l10n cascade_tags %}{% spaceless %}\n\n{% with css_classes=instance.css_classes %}\n<div uib-carousel{% if css_clas"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/angular-ui/tabset.html",
    "chars": 354,
    "preview": "{% load static cascade_tags %}\n{% spaceless %}{% with justified=instance.glossary.justified %}\n\n<uib-tabset{% if justifi"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/button.html",
    "chars": 429,
    "preview": "{% extends \"cascade/link/link-base.html\" %}\n{% load sekizai_tags %}\n\n{% block link_link %}{% with instance_link=instance"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/card.html",
    "chars": 318,
    "preview": "{% load cascade_tags %}{% spaceless %}\n{% with inline_styles=instance.inline_styles %}\n<div class=\"{{ instance.css_class"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/carousel-slide.html",
    "chars": 240,
    "preview": "{% include \"cascade/bootstrap4/picture.html\" %}\n{% load cascade_tags %}\n{% if instance.num_children %}\n<div class=\"carou"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/carousel.html",
    "chars": 1192,
    "preview": "{% load l10n cascade_tags %}\n{% localize off %}{% spaceless %}{% with bootstrap_element=\"carousel\" inline_styles=instanc"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/framedicon.html",
    "chars": 732,
    "preview": "{% load sekizai_tags %}\n{% if stylesheet_url %}{% addtoblock \"css\" %}<link href=\"{{ stylesheet_url }}\" rel=\"stylesheet\" "
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/image.html",
    "chars": 1020,
    "preview": "{% load l10n static cascade_tags thumbnail %}\n{% localize off %}{% spaceless %}{% with css_classes=instance.css_classes "
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/jumbotron.html",
    "chars": 1504,
    "preview": "{% load l10n cascade_tags thumbnail sekizai_tags %}\n\n{% addtoblock \"css\" %}{% localize off %}\n<style>\n{% for elem in ins"
  },
  {
    "path": "cmsplugin_cascade/templates/cascade/bootstrap4/linked-image.html",
    "chars": 189,
    "preview": "{% extends \"cascade/link/link-base-nostyle.html\" %}\n{% spaceless %}\n\n{% block link_content %}\n\t{% include \"cascade/boots"
  }
]

// ... and 116 more files (download for full content)

About this extraction

This page contains the full source code of the jrief/djangocms-cascade GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 316 files (1.0 MB), approximately 260.2k tokens, and a symbol index with 866 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!