diff --git a/.editorconfig b/.editorconfig index 5e3132f1b..3495fd660 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ insert_final_newline = false trim_trailing_whitespace = false # Set default charset -[*.{js,py,rst,json,yml,html,txt,scss}] +[*.{js,py,rst,json,yml,html,txt,css,scss}] charset = utf-8 [*.{js,py,rst,yml,html,scss}] @@ -18,7 +18,7 @@ line_length=119 indent_style = space indent_size = 4 -[*.{html,js,rst,scss}] +[*.{html,js,rst,css,scss}] indent_style = tab indent_size = 4 diff --git a/.gitignore b/.gitignore index 682b185ac..98f3a88f9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ workdir/ filer_public/ filer_public_thumbnails/ examples/bs3demo/private_settings.py +.venv/ +poetry.lock diff --git a/cmsplugin_cascade/__init__.py b/cmsplugin_cascade/__init__.py index aa31e550b..49d128497 100644 --- a/cmsplugin_cascade/__init__.py +++ b/cmsplugin_cascade/__init__.py @@ -19,6 +19,6 @@ 15. git commit -m 'Start with ' 16. git push """ -__version__ = "1.2.3" +__version__ = "1.3.dev" default_app_config = 'cmsplugin_cascade.apps.CascadeConfig' diff --git a/cmsplugin_cascade/app_settings.py b/cmsplugin_cascade/app_settings.py index 6618a3600..7ae91b186 100644 --- a/cmsplugin_cascade/app_settings.py +++ b/cmsplugin_cascade/app_settings.py @@ -21,7 +21,7 @@ def CMSPLUGIN_CASCADE(self): from django.forms.fields import NumberInput from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy - from cmsplugin_cascade.fields import (ColorField, SelectTextAlignField, SelectOverflowField, SizeField, + from cmsplugin_cascade.fields import (ColorFieldExtra, SelectTextAlignField, SelectOverflowField, SizeField, BorderChoiceField) if hasattr(self, '_config_CMSPLUGIN_CASCADE'): @@ -96,7 +96,7 @@ def CMSPLUGIN_CASCADE(self): (['line-height'], NumberInput)) extra_inline_styles.setdefault( 'Colors', - (['color', 'background-color'], ColorField)) + (['color', 'background-color'], ColorFieldExtra)) extra_inline_styles.setdefault( 'Border', (['border', 'border-top', 'border-right', 'border-bottom', 'border-left'], BorderChoiceField)) @@ -130,7 +130,18 @@ def CMSPLUGIN_CASCADE(self): config.setdefault('cache_strides', True) + config.setdefault('merge_extra_fields',True) + + config.setdefault('fallback',{ + # default + 'path_main_scss': 'bs4demo/css/main.scss', + 'image':{'color':'hsla(221.7, 57.5%, 84.3%, 0.8)', 'svg':''}, + 'picture':{'color':'hsla(0, 40%, 80.4%, 0.8)', 'svg':'' }, + 'jumbotron':{'color':'hsl(62, 90%, 90%, 0.8)', 'svg':''}, + }) + config.setdefault('register_page_editor', True) + for module_name in self.CASCADE_PLUGINS: try: diff --git a/cmsplugin_cascade/bootstrap4/accordion.py b/cmsplugin_cascade/bootstrap4/accordion.py index 50d0d6743..ab831d0cf 100644 --- a/cmsplugin_cascade/bootstrap4/accordion.py +++ b/cmsplugin_cascade/bootstrap4/accordion.py @@ -10,7 +10,7 @@ from cmsplugin_cascade.plugin_base import TransparentWrapper, TransparentContainer from cmsplugin_cascade.widgets import NumberInputWidget from .plugin_base import BootstrapPluginBase - +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested class AccordionFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): num_children = IntegerField( @@ -35,6 +35,9 @@ class AccordionFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): help_text=_("Start with the first card open.") ) + if used_compact_form: + entangled_nested(num_children, close_others,first_is_open, data_nested='accordion') + class Meta: untangled_fields = ['num_children'] entangled_fields = {'glossary': ['close_others', 'first_is_open']} @@ -85,6 +88,9 @@ class AccordionGroupFormMixin(EntangledModelFormMixin): help_text=_("Add standard padding to card body."), ) + if used_compact_form: + entangled_nested(heading,body_padding, data_nested='accordion_group') + class Meta: entangled_fields = {'glossary': ['heading', 'body_padding']} diff --git a/cmsplugin_cascade/bootstrap4/buttons.py b/cmsplugin_cascade/bootstrap4/buttons.py index 282d74cb6..dc4649aac 100644 --- a/cmsplugin_cascade/bootstrap4/buttons.py +++ b/cmsplugin_cascade/bootstrap4/buttons.py @@ -9,7 +9,10 @@ from cmsplugin_cascade.icon.forms import IconFormMixin from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin from cmsplugin_cascade.link.plugin_base import LinkElementMixin +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested +from sass_processor.processor import sass_processor +sass_processor('cascade/css/admin/compact_forms/bootstrap4-colors.scss') class ButtonTypeWidget(widgets.RadioSelect): """ @@ -24,7 +27,6 @@ class ButtonSizeWidget(widgets.RadioSelect): """ template_name = 'cascade/admin/legacy_widgets/button_sizes.html' if DJANGO_VERSION < (2, 0) else 'cascade/admin/widgets/button_sizes.html' - class ButtonFormMixin(EntangledModelFormMixin): BUTTON_TYPES = [ ('btn-primary', _("Primary")), @@ -104,6 +106,12 @@ class ButtonFormMixin(EntangledModelFormMixin): help_text=_("Add an Icon before or after the button content."), ) + if used_compact_form: + entangled_nested(link_content,button_type,button_size,button_options,\ + stretched_link, data_nested='button') + entangled_nested(button_type, data_nested='button', template_key='button_type') + entangled_nested(icon_align, data_nested='icon') + class Meta: entangled_fields = {'glossary': ['link_content', 'button_type', 'button_size', 'button_options', 'icon_align', 'stretched_link']} @@ -111,13 +119,14 @@ class Meta: class BootstrapButtonMixin(IconPluginMixin): require_parent = True - parent_classes = ['BootstrapColumnPlugin', 'SimpleWrapperPlugin'] + parent_classes = ['BootstrapColumnPlugin', 'SimpleWrapperPlugin','BootstrapNavItemsPlugin', 'BootstrapListsPlugin'] 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'] @@ -148,6 +157,7 @@ class BootstrapButtonPlugin(BootstrapButtonMixin, LinkPluginBase): form = BootstrapButtonFormMixin ring_plugin = 'ButtonPlugin' DEFAULT_BUTTON_ATTRIBUTES = {'role': 'button'} +# render_template_fallback = "cascade/generic/fallback_button.html" # mode stride gallery class Media: js = ['admin/js/jquery.init.js', 'cascade/js/admin/buttonplugin.js'] @@ -168,6 +178,9 @@ 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') + if hasattr(obj, 'parent') and hasattr(obj.parent,'parent') and hasattr(obj.parent.parent,'plugin_type'): + if obj.parent.parent.plugin_type == 'BootstrapNavBrandPlugin': + css_classes.insert(0,'nav-link text-left') return css_classes @classmethod diff --git a/cmsplugin_cascade/bootstrap4/carousel.py b/cmsplugin_cascade/bootstrap4/carousel.py index a838d5e0a..53f46be16 100644 --- a/cmsplugin_cascade/bootstrap4/carousel.py +++ b/cmsplugin_cascade/bootstrap4/carousel.py @@ -14,6 +14,7 @@ from cmsplugin_cascade.bootstrap4.utils import IMAGE_RESIZE_OPTIONS from cmsplugin_cascade.forms import ManageChildrenFormMixin from cmsplugin_cascade.image import ImagePropertyMixin, ImageFormMixin +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested logger = logging.getLogger('cascade') @@ -55,6 +56,9 @@ class CarouselSlidesFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): initial=['upscale', 'crop', 'subject_location', 'high_resolution'], ) + if used_compact_form: + entangled_nested(resize_options, container_max_heights,options,interval,num_children, data_nested='carousel') + class Meta: untangled_fields = ['num_children'] entangled_fields = {'glossary': ['interval', 'options', 'container_max_heights', 'resize_options']} diff --git a/cmsplugin_cascade/bootstrap4/container.py b/cmsplugin_cascade/bootstrap4/container.py index e7fdbc463..9f650609c 100644 --- a/cmsplugin_cascade/bootstrap4/container.py +++ b/cmsplugin_cascade/bootstrap4/container.py @@ -11,6 +11,7 @@ from cmsplugin_cascade import app_settings from cmsplugin_cascade.bootstrap4.grid import Breakpoint from cmsplugin_cascade.forms import ManageChildrenFormMixin +from cmsplugin_cascade.helpers import entangled_nested, used_compact_form from .plugin_base import BootstrapPluginBase from . import grid @@ -19,17 +20,26 @@ 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))) + if not used_compact_form: + 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))) else: - widget_choices.append((bp.name, "{} (≥{:.1f}px and <{:.1f}px)".format(bp.label, bound.min, bound.max))) + if index == 0: + widget_choices.append((bp.name, "{} <{:.1f}px".format(bp._name_, bound.max))) + else: + widget_choices.append((bp.name, "{} ≥{:.1f}px".format(bp._name_, bound.min))) return widget_choices class ContainerBreakpointsWidget(widgets.CheckboxSelectMultiple): - template_name = 'cascade/admin/legacy_widgets/container_breakpoints.html' if DJANGO_VERSION < (2, 0) else 'cascade/admin/widgets/container_breakpoints.html' + if not used_compact_form: + template_name = 'cascade/admin/legacy_widgets/container_breakpoints.html' if DJANGO_VERSION < (2, 0) else 'cascade/admin/widgets/container_breakpoints.html' + else: + template_name = 'cascade/admin/compact_forms/widgets/container_breakpoints.html' class ContainerFormMixin(EntangledModelFormMixin): @@ -48,6 +58,9 @@ class ContainerFormMixin(EntangledModelFormMixin): help_text=_("Changing your outermost '.container' to '.container-fluid'.") ) + if used_compact_form: + entangled_nested( breakpoints, fluid, data_nested="container") + class Meta: entangled_fields = {'glossary': ['breakpoints', 'fluid']} @@ -121,6 +134,9 @@ class BootstrapRowFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): help_text=_('Number of columns to be created with this row.'), ) + if used_compact_form: + entangled_nested( num_children, data_nested="row") + class Meta: untangled_fields = ['num_children'] @@ -196,7 +212,7 @@ def choose_help_text(*phrases): 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: @@ -325,6 +341,13 @@ def choose_help_text(*phrases): glossary_fields.extend(reorder_fields.keys()) glossary_fields.extend(responsive_fields.keys()) + + if used_compact_form: + entangled_nested(width_fields, data_nested="column", template_key="column", ) + entangled_nested(offset_fields, data_nested="offset", template_key="column", ) + entangled_nested(reorder_fields, data_nested="reorder", template_key="column",) + entangled_nested(responsive_fields, data_nested="responsive", template_key="column") + class Meta: entangled_fields = {'glossary': glossary_fields} diff --git a/cmsplugin_cascade/bootstrap4/icon.py b/cmsplugin_cascade/bootstrap4/icon.py index 0a47647ad..4927e8fac 100644 --- a/cmsplugin_cascade/bootstrap4/icon.py +++ b/cmsplugin_cascade/bootstrap4/icon.py @@ -8,7 +8,7 @@ from cmsplugin_cascade.link.plugin_base import LinkElementMixin from cmsplugin_cascade.icon.forms import IconFormMixin from cmsplugin_cascade.icon.plugin_base import IconPluginMixin - +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested class FramedIconFormMixin(IconFormMixin): SIZE_CHOICES = [('{}em'.format(c), "{} em".format(c)) for c in range(1, 13)] @@ -56,6 +56,10 @@ class FramedIconFormMixin(IconFormMixin): required=False, ) + if used_compact_form: + entangled_nested(font_size, color, background_color, text_align, border, + border_radius, data_nested='icon') + class Meta: entangled_fields = {'glossary': ['font_size', 'color', 'background_color', 'text_align', 'border', 'border_radius']} diff --git a/cmsplugin_cascade/bootstrap4/image.py b/cmsplugin_cascade/bootstrap4/image.py index d3f536e95..ab4b93396 100644 --- a/cmsplugin_cascade/bootstrap4/image.py +++ b/cmsplugin_cascade/bootstrap4/image.py @@ -10,6 +10,7 @@ from cmsplugin_cascade.fields import SizeField from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin from cmsplugin_cascade.link.plugin_base import LinkElementMixin +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested logger = logging.getLogger('cascade.bootstrap4') @@ -67,6 +68,10 @@ class BootstrapImageFormMixin(ImageFormMixin): help_text=_("How to align a non-responsive image."), ) + if used_compact_form: + entangled_nested(image_shapes, image_width_responsive, image_width_fixed, + image_height, resize_options, image_alignment, data_nested='image_setting') + class Meta: entangled_fields = {'glossary': ['image_shapes', 'image_width_responsive', 'image_width_fixed', 'image_height', 'resize_options', 'image_alignment']} diff --git a/cmsplugin_cascade/bootstrap4/jumbotron.py b/cmsplugin_cascade/bootstrap4/jumbotron.py index ae3eb213f..4ba33e2b6 100644 --- a/cmsplugin_cascade/bootstrap4/jumbotron.py +++ b/cmsplugin_cascade/bootstrap4/jumbotron.py @@ -12,6 +12,7 @@ from cmsplugin_cascade.bootstrap4.container import ContainerGridMixin from cmsplugin_cascade.bootstrap4.fields import BootstrapMultiSizeField from cmsplugin_cascade.bootstrap4.picture import get_picture_elements +from cmsplugin_cascade.helpers import entangled_nested, used_compact_form logger = logging.getLogger('cascade') @@ -154,6 +155,11 @@ class JumbotronFormMixin(EntangledModelFormMixin): help_text=_("This property specifies the width and height of a background image in px or %."), ) + if used_compact_form: + entangled_nested(fluid, background_color, element_heights, image_file, + background_repeat, background_attachment, + background_vertical_position, background_horizontal_position, + background_size, background_width_height, data_nested='jumbotron') class Meta: entangled_fields = {'glossary': ['fluid', 'background_color', 'element_heights', 'image_file', 'background_repeat', 'background_attachment', @@ -195,6 +201,7 @@ class BootstrapJumbotronPlugin(BootstrapPluginBase): form = JumbotronFormMixin raw_id_fields = ['image_file'] render_template = 'cascade/bootstrap4/jumbotron.html' + render_template_fallback = "cascade/generic/fallback_jumbotron.html" # mode stride gallery ring_plugin = 'JumbotronPlugin' footnote_html = """

For more information about the Jumbotron please read the diff --git a/cmsplugin_cascade/bootstrap4/lists.py b/cmsplugin_cascade/bootstrap4/lists.py new file mode 100644 index 000000000..d715d0271 --- /dev/null +++ b/cmsplugin_cascade/bootstrap4/lists.py @@ -0,0 +1,64 @@ +from django.utils.html import format_html +from django.utils.translation import ugettext_lazy as _ +from .plugin_base import BootstrapPluginBase +from django.forms.fields import ChoiceField +from cms.plugin_pool import plugin_pool +from entangled.forms import EntangledModelFormMixin + +import logging +logger = logging.getLogger('cascade') + +class BootstrapListsMixin( EntangledModelFormMixin): + list_options = ChoiceField( + label=_("List Options"), + choices=[ + ('inherit', _("inherit")), + ('inline-item', _('ul(class="inline-item").li(class="list-inline-item")')), + ('navbar-nav', _('ul(class="navbar-nav").li(class="nav-item")')), + ], + required=False, + ) + class Meta: + entangled_fields = {'glossary': ['list_options']} + + +@plugin_pool.register_plugin +class BootstrapListsPlugin(BootstrapPluginBase): + name = _("Lists") + alien_plugins = True + form = BootstrapListsMixin + alien_child_classes = True + render_template = 'cascade/bootstrap4/navbar_list.html' + default_css_class = '' + require_parent = False + + @classmethod + def get_css_classes(cls, obj): + css_classes = cls.super(BootstrapListsPlugin, cls).get_css_classes(obj) + list_options = [obj.glossary.get('list_options')] if obj.glossary.get('list_options') else '' + if list_options: + for opts in list_options: + css_classes.append(opts) + return css_classes + + + @classmethod + def get_identifier(cls, obj): + identifier = super(BootstrapListsPlugin, cls).get_identifier(obj) + if hasattr(cls,'default_css_class'): + css_classes_without_default = obj.css_classes.replace( cls.default_css_class , '' , 1) + else: + css_classes_without_default = obj.css_classes + return format_html('

{0}{1}
', + identifier, css_classes_without_default ) + + @classmethod + def sanitize_model(cls, obj): + list_child_css_classes = obj.glossary['child_css_classes'].split(' ') if 'child_css_classes' in obj.glossary else [] + list_options = 'list-inline-item' if obj.glossary.get('list_options') else '' + list_options = 'nav-item' if obj.glossary.get('list_options') == 'navbar-nav' else list_options + if list_options: + list_child_css_classes.append(list_options) + obj.glossary['child_css_classes'] = ' '.join(list_child_css_classes) + super().sanitize_model(obj) + diff --git a/cmsplugin_cascade/bootstrap4/mixins.py b/cmsplugin_cascade/bootstrap4/mixins.py index 029bf53b0..3063576ad 100644 --- a/cmsplugin_cascade/bootstrap4/mixins.py +++ b/cmsplugin_cascade/bootstrap4/mixins.py @@ -4,7 +4,7 @@ from entangled.forms import EntangledModelFormMixin from cmsplugin_cascade.utils import CascadeUtilitiesMixin from cmsplugin_cascade.bootstrap4.grid import Breakpoint - +from cmsplugin_cascade.helpers import entangled_nested, used_compact_form class BootstrapUtilities(type): """ @@ -26,23 +26,53 @@ class BootstrapUtilities(type): 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 + specially useful to add CSS classes or HTML data attributes from the utilities section of Bootstrap-4, such as margins, borders, colors, etc. + + The 'property_name' attritbute in property methods is needed because python property methods don't have name + attributes without using inspect module or others things. + The 'attrs_type' attritbute in property methods can have two possiblity values 'css_classes' or 'html_data_attrs'. + The 'anchors_fields' in the property_fields attributes can add choices id elements of the current page, theses + choices are realy set when the request is available. """ + def __new__(cls, *args): form_fields = {} + form_fields_by_property_name = {} + form_fields_by_attr_type = {} + fields_choices_anchors = [] + for arg in args: if isinstance(arg, property): - form_fields.update(arg.fget(cls)) + property_fields = arg.fget(cls) + form_subfields = property_fields['form_fields'] + attrs_type = property_fields['attrs_type'] + property_name = property_fields['property_name'] + + form_fields.update(form_subfields) + form_fields_by_property_name[property_name]= property_fields['form_fields'] + + form_fields_by_attr_type.setdefault(attrs_type, []) + form_fields_by_attr_type[attrs_type ].extend(property_fields['form_fields'].keys()) + + if 'anchors_fields' in property_fields: + fields_choices_anchors.extend(property_fields['anchors_fields']) + + if used_compact_form: + for property_name , field in form_fields_by_property_name.items(): + entangled_nested(field, data_nested=property_name , template_key=property_name) 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}) + utility_form_mixin = type('UtilitiesFormMixin', (EntangledModelFormMixin,), dict(form_fields, Meta=Meta) ) + return type('HtmlAttrsUtilitiesMixin', (CascadeUtilitiesMixin,), {'utility_form_mixin': utility_form_mixin, + 'attr_type': form_fields_by_attr_type , 'fields_with_choices_anchors': fields_choices_anchors }) @property def background_and_color(cls): + attrs_type = 'css_classes' + property_name = 'background_and_color' choices = [ ('', _("Default")), ('bg-primary text-white', _("Primary with white text")), @@ -57,15 +87,19 @@ def background_and_color(cls): ('bg-transparent text-dark', _("Transparent with dark text")), ('bg-transparent text-white', _("Transparent with white text")), ] - return {'background_and_color': ChoiceField( + form_fields = {'background_and_color': ChoiceField( label=_("Background and color"), choices=choices, required=False, initial='', )} + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields @property def margins(cls): + attrs_type = 'css_classes' + property_name = 'margins' form_fields = {} choices_format = [ ('m-{}{}', _("4 sided margins ({})")), @@ -91,10 +125,13 @@ def margins(cls): required=False, initial='', ) - return form_fields + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields @property def vertical_margins(cls): + attrs_type = 'css_classes' + property_name = 'vertical_margins' form_fields = {} choices_format = [ ('my-{}{}', _("Vertical margins ({})")), @@ -116,10 +153,13 @@ def vertical_margins(cls): required=False, initial='', ) - return form_fields + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields @property def paddings(cls): + attrs_type = 'css_classes' + property_name = 'paddings' form_fields = {} choices_format = [ ('p-{}{}', _("4 sided padding ({})")), @@ -145,11 +185,14 @@ def paddings(cls): required=False, initial='', ) - return form_fields + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields @property def floats(cls): form_fields = {} + attrs_type = 'css_classes' + property_name = 'floats' choices_format = [ ('float-{}none', _("Do not float")), ('float-{}left', _("Float left")), @@ -169,4 +212,122 @@ def floats(cls): required=False, initial='', ) - return form_fields + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields + + + @property + def flex_directions(cls): + form_fields = {} + attrs_type = 'css_classes' + property_name = 'flex_directions' + choices_format = [ + ('flex-{}row', _("horizontal")), + ('flex-{}row-reverse', _("horizontal reverse")), + ('flex-{}column', _("Vertical")), + ('flex-{}column-reverse', _("Vertical reverse")), + ] + for bp in Breakpoint.range(Breakpoint.xs, Breakpoint.xl): + if bp == Breakpoint.xs: + choices = [ (c.format(''), l ) for c, l in choices_format] + choices.insert(0, ('', _("No Flex Directions"))) + else: + choices = [(c.format(bp.name + '-'), l) for c, l in choices_format] + choices.insert(0, ('', _("Inherit from above"))) + form_fields['Flex_{}'.format(bp.name)] = ChoiceField( + label=format_lazy(_("Flex Directions for {breakpoint}"), breakpoint=bp.label), + choices=choices, + required=False, + initial='' + ) + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields + + @property + def display_propertys(cls): + form_fields = {} + attrs_type = 'css_classes' + property_name = 'display_propertys' + choices_format = [ + ('d-{}{}', _("horizontal")), + ] + notation = ['none', 'inline', 'inline-block', 'block', 'table', 'table-cell', 'table-row', 'flex', 'inline-flex'] + for bp in Breakpoint.range(Breakpoint.xs, Breakpoint.xl): + if bp == Breakpoint.xs: + choices = [(c.format('', n), c.format('', n)) for c, l in choices_format for n in notation] + choices.insert(0, ('', _("No Display Propertys"))) + else: + choices = [(c.format(bp.name + '-', n), c.format(bp.name + '-', n)) for c, l in choices_format for n in notation] + choices.insert(0, ('', _("Inherit from above"))) + form_fields['Flex_{}'.format(bp.name)] = ChoiceField( + label=format_lazy(_("Flex Directions for {breakpoint}"), breakpoint=bp.label), + choices=choices, + required=False, + initial='' + ) + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields + + @property + def justify_content(cls): + form_fields = {} + attrs_type = 'css_classes' + property_name = 'justify_content' + choices_format = [ + ('justify-content-{}{}', _("Justify Content")), + ] + notation = [ 'start', 'end', 'center', 'between', 'around'] + for bp in Breakpoint.range(Breakpoint.xs, Breakpoint.xl): + if bp == Breakpoint.xs: + choices = [(c.format('', n), c.format('', n)) for c, l in choices_format for n in notation] + choices.insert(0, ('', _("No Justify content"))) + else: + choices = [(c.format(bp.name + '-', n), + c.format(bp.name + '-', n)) for c, l in choices_format for n in notation] + choices.insert(0, ('', _("Inherit from above"))) + form_fields['Justify_content_{}'.format(bp.name)] = ChoiceField( + label=format_lazy(_("Justify Content for {breakpoint}"), breakpoint=bp.label), + choices=choices, + required=False, + initial='' + ) + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields + + @property + def positions(cls): + form_fields = {} + attrs_type = 'css_classes' + property_name = 'positions' + choices_format = [ + ('{}', _("Position")), + ] + notation = ['inherit', 'fixed-top' , 'fixed-bottom' , 'sticky-top'] + choices = [ (str(n), str(n)) for c, l in choices_format for n in notation] + form_fields['Position'] = ChoiceField( + label=format_lazy(_("Position")), + choices=choices, + required=False, + initial='' + ) + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields + + @property + def list_inline(cls): + form_fields = {} + attrs_type = 'css_classes' + property_name = 'list_inline' + choices_format = [ + ('{}', _("List inline")), + ] + notation = ['inherit', 'fixed-top' , 'fixed-bottom' , 'sticky-top'] + choices = [ (str(n), str(n)) for c, l in choices_format for n in notation] + form_fields['Position'] = ChoiceField( + label=format_lazy(_("Position")), + choices=choices, + required=False, + initial='' + ) + property_fields = { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } + return property_fields diff --git a/cmsplugin_cascade/bootstrap4/navbar.py b/cmsplugin_cascade/bootstrap4/navbar.py new file mode 100644 index 000000000..db55ae582 --- /dev/null +++ b/cmsplugin_cascade/bootstrap4/navbar.py @@ -0,0 +1,275 @@ +from django.utils.html import format_html +from django.utils.translation import ugettext_lazy as _ +from cmsplugin_cascade.fields import SizeField +from .plugin_base import BootstrapPluginBase +from cmsplugin_cascade.bootstrap4.jumbotron import ImageBackgroundMixin, JumbotronFormMixin +from cmsplugin_cascade import app_settings +from cmsplugin_cascade.bootstrap4.container import ContainerFormMixin, ContainerGridMixin +from cmsplugin_cascade.image import ImageFormMixin, ImagePropertyMixin + +from cmsplugin_cascade.bootstrap4.container import get_widget_choices, ContainerBreakpointsWidget +from cmsplugin_cascade.bootstrap4.image import get_image_tags +from .grid import Breakpoint +from cmsplugin_cascade.link.config import LinkPluginBase +from django.forms import widgets +from django.forms.fields import BooleanField, CharField, ChoiceField + +from cms.plugin_pool import plugin_pool +from entangled.forms import EntangledModelFormMixin +from django.core.exceptions import ValidationError + +import logging +logger = logging.getLogger('cascade') + +class BootstrapNavbarFormMixin(EntangledModelFormMixin): + OPTION_NAV_COLLAPSE = [(c, c) for c in [ "inherit", "navbar-expand","navbar-expand-sm", "navbar-expand-md","navbar-expand-lg", "navbar-expand-xl"] ] + OPTION_NAV_COLOR = [(c, c) for c in [ "inherit", "navbar-light", "navbar-dark"]] + OPTION_NAV_BG_COLOR = [ "inherit", "bg-primary", "bg-secondary","bg-success", "bg-danger", "bg-warning", "bg-info" ,"bg-light", "bg-dark" , "bg-white", "bg-transparent"] + OPTION_NAV_BG_GRADIENT = [ "bg-gradient-primary", "bg-gradient-secondary", "bg-gradient-success", "bg-gradient-danger", "bg-gradient-warning", "bg-gradient-info", "bg-gradient-light", "bg-gradient-dark"] + OPTION_NAV_BG_MIX = OPTION_NAV_BG_COLOR + OPTION_NAV_BG_GRADIENT + OPTION_NAV_PLACEMENTS=["inherit", "fixed-top" , "fixed-bottom" , "sticky-top"] + + navbar_collapse = ChoiceField( + label=_('Navbar collapse'), + choices=OPTION_NAV_COLLAPSE, + help_text=_("Adjust interval for the navbar_collapse.") + ) + + navbar_color = ChoiceField( + label=_('Navbar text color'), + choices=OPTION_NAV_COLOR, + help_text=_("Adjust the navbar color.") + ) + + navbar_bg_color = ChoiceField( + label=_('Navbar bg color'), + choices=[(c, c) for c in OPTION_NAV_BG_MIX ], + help_text=_("Adjust interval for the navbar background color."), + ) + + navbar_placement = ChoiceField( + label=_('navbar-place'), + choices=[(c, c) for c in OPTION_NAV_PLACEMENTS], + help_text=_("Adjust position ('fixed-top or fixed-button need to be set in Jumbotron if it is a plugin parent.')") + ) + + class Meta: + entangled_fields = {'glossary':['navbar_collapse', 'navbar_color', 'navbar_bg_color', 'navbar_placement']} + + 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() + return cleaned_data + + +@plugin_pool.register_plugin +class BootstrapNavbarPlugin(BootstrapPluginBase): + name = _("Navbar") + parent_classes = None + require_parent = False + model_mixins = (ContainerGridMixin,) + default_css_class = 'navbar' + default_css_attributes = ('options') + alien_child_classes = True + render_template = 'cascade/bootstrap4/navbar.html' + ring_plugin = 'BootstrapNavbarPlugin' + fixed_top_and_toolbar = None + + class Media: + js = ['cascade/js/admin/navbarplugin.js'] + + def get_form(self, request, obj=None, **kwargs): + kwargs['form'] = BootstrapNavbarFormMixin + if hasattr(obj, 'glossary'): + if obj.glossary.get('navbar_placement') == 'fixed-top': + obj.fixed_top_and_toolbar=True + return super().get_form(request, obj, **kwargs) + + @classmethod + def sanitize_model(cls, obj): + sanitized = False + if 'Position' in obj.get_parent_glossary(): + obj.glossary['position_jumbotron'] = obj.get_parent_glossary()['Position'] + if hasattr(obj,'fixed_top_and_toolbar'): + navbar_placement = obj.glossary.get('navbar_placement') + del obj.fixed_top_and_toolbar + super().sanitize_model(obj) + return sanitized + + + @classmethod + def get_css_classes(cls, obj, ): + css_classes = cls.super(BootstrapNavbarPlugin, cls).get_css_classes(obj) + navbar_collapse = obj.glossary.get('navbar_collapse', '') + navbar_color = obj.glossary.get('navbar_color', '') + navbar_bg_color = obj.glossary.get('navbar_bg_color', '') + navbar_placement = obj.glossary.get('navbar_placement', '') + if navbar_collapse != 'inherit': + css_classes.append(navbar_collapse) + if navbar_color != 'inherit': + css_classes.append(navbar_color) + if navbar_bg_color != 'inherit': + css_classes.append(navbar_bg_color) + if navbar_placement != 'inherit': + css_classes.append(navbar_placement) + return css_classes + + @classmethod + def get_identifier(cls, obj): + identifier = super().get_identifier(obj) + css_classes_without_default = obj.css_classes.replace( cls.default_css_class ,'',1) + return format_html('
{0}{1}
', + identifier, css_classes_without_default) + + + +@plugin_pool.register_plugin +class BootstrapNavBrandPlugin(LinkPluginBase): + name = _("Nav brand") + parent_classes = ['BootstrapNavbarPlugin'] + render_template = 'cascade/bootstrap4/navbar_brand.html' + raw_id_fields = LinkPluginBase.raw_id_fields + ['image_file'] + default_css_class = '' + require_parent = False + allow_children = True + alien_child_classes = True + + @classmethod + def get_child_css_classes(cls, obj): + child_css_classes = cls.super(BootstrapNavBrandPlugin, cls).get_child_css_classes(obj) + child_css_classes = obj.glossary.get('child_css_class') + if child_css_class: + child_css_classes.append(css_class) + return css_classes + + @classmethod + def get_identifier(cls, obj): + identifier = super(BootstrapNavBrandPlugin, cls).get_identifier(obj) + css_classes_without_default = obj.css_classes.replace( cls.default_css_class,'',1) + return format_html('
{0}{1}
', + identifier, css_classes_without_default) + + +class BootstrapNavBrandThumbImageFormMixin(EntangledModelFormMixin): + image_width_fixed = SizeField( + label=_("Fixed Image Width"), + allowed_units=['px'], + required = False, + help_text=_("Set a fixed image width in pixels.") + ) + + class Meta: + entangled_fields = {'glossary': ['image_width_fixed']} + +class BootstrapNavBrandImageFormMixin(ImageFormMixin, BootstrapNavBrandThumbImageFormMixin): + pass + + +@plugin_pool.register_plugin +class BootstrapNavBrandImagePlugin(BootstrapPluginBase): + name = _("Nav brand Image") + model_mixins = (ImagePropertyMixin,) + parent_classes = ['BootstrapNavBrandPlugin', 'BootstrapListsPlugin'] + allow_children = True + alien_child_classes = True + html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'} + raw_id_fields = ['image_file'] + SIZE_CHOICES = ('auto', 'width/height', 'cover', 'contain') + form = BootstrapNavBrandImageFormMixin + raw_id_fields = ['image_file'] + render_template = 'cascade/bootstrap4/navbar_brand_image.html' + default_css_class = 'nav-brand-logo-ts' + + def render(self, context, instance, placeholder, tags=None): + try: + tags = get_image_tags(instance) + except Exception as exc: + logger.warning("Unable generate image tags. Reason: {}".format(exc)) + tags = tags if tags else {} + if 'extra_styles' in tags: + extra_styles = tags.pop('extra_styles') + inline_styles = instance.glossary.get('inline_styles', {}) + inline_styles.update(extra_styles) + instance.glossary['inline_styles'] = inline_styles + context.update(dict(instance=instance, placeholder=placeholder, **tags)) + return context + + @classmethod + def get_css_classes(cls, obj): + css_classes = cls.super(BootstrapNavBrandImagePlugin, 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_child_css_classes(cls, obj): + child_css_classes = cls.super(BootstrapNavBrandImagePlugin, cls).get_child_css_classes(obj) + child_css_classes = obj.glossary.get('child_css_class') + if child_css_class: + child_css_classes.append(css_class) + return css_classes + + +@plugin_pool.register_plugin +class BootstrapNavCollapsePlugin(BootstrapPluginBase): + name = _("Nav Collapse") + parent_classes = ['BootstrapNavbarPlugin'] + render_template = 'cascade/bootstrap4/navbar_collapse.html' + default_css_class = 'collapse navbar-collapse' + + @classmethod + def get_css_classes(cls, obj): + css_classes = cls.super( BootstrapNavCollapsePlugin, cls).get_css_classes(obj) + return css_classes + + @classmethod + def get_identifier(cls, obj): + identifier = super(BootstrapNavCollapsePlugin, cls).get_identifier(obj) + css_classes_without_default = obj.css_classes.replace( cls.default_css_class,'',1) + return format_html('
{0}{1}
', + identifier, css_classes_without_default) + + @classmethod + def sanitize_model(cls, obj): + sanitized = super().sanitize_model(obj) + return sanitized + +@plugin_pool.register_plugin +class BootstrapNavItemsMainMenuPlugin(BootstrapPluginBase): + name = _("NavItems MainMenu ") + parent_classes = ['BootstrapListsPlugin'] + require_parent = False + allow_children = False + alien_child_classes = True + render_template = 'cascade/bootstrap4/navbar_nav_items_li_menu_main_links.html' + + @classmethod + def get_css_classes(cls, obj): + css_classes = cls.super(BootstrapNavItemsMainMenuPlugin, cls).get_css_classes(obj) + return css_classes + + @classmethod + def get_identifier(cls, obj): + identifier = super(BootstrapNavItemsMainMenuPlugin, cls).get_identifier(obj) + if hasattr(cls,'default_css_class'): + css_classes_without_default = obj.css_classes.replace( cls.default_css_class,'',1) + else: + css_classes_without_default = obj.css_classes + return format_html('
{0}{1}
', + identifier, css_classes_without_default) + + +@plugin_pool.register_plugin +class BootstrapNavbarToogler(BootstrapPluginBase): + name = _("Nav toogler") + default_css_class = 'navbar-toggler' + parent_classes = ['BootstrapNavbarPlugin'] + render_template = 'cascade/bootstrap4/navbar_toogler.html' diff --git a/cmsplugin_cascade/bootstrap4/picture.py b/cmsplugin_cascade/bootstrap4/picture.py index a939968ff..57cf158e8 100644 --- a/cmsplugin_cascade/bootstrap4/picture.py +++ b/cmsplugin_cascade/bootstrap4/picture.py @@ -10,6 +10,7 @@ from cmsplugin_cascade.image import ImageFormMixin, ImagePropertyMixin from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin from cmsplugin_cascade.link.plugin_base import LinkElementMixin +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested logger = logging.getLogger('cascade.bootstrap4') @@ -48,6 +49,9 @@ class BootstrapPictureFormMixin(ImageFormMixin): initial=['img-fluid'] ) + if used_compact_form: + entangled_nested(responsive_heights, responsive_zoom, resize_options, image_shapes, data_nested='picture') + class Meta: entangled_fields = {'glossary': ['responsive_heights', 'responsive_zoom', 'resize_options', 'image_shapes']} @@ -127,4 +131,4 @@ def sanitize_model(cls, obj): return return sanitized -plugin_pool.register_plugin(BootstrapPicturePlugin) +plugin_pool.register_plugin(BootstrapPicturePlugin) \ No newline at end of file diff --git a/cmsplugin_cascade/bootstrap4/settings.py b/cmsplugin_cascade/bootstrap4/settings.py index 3c3e2db2a..2da7dcd8a 100644 --- a/cmsplugin_cascade/bootstrap4/settings.py +++ b/cmsplugin_cascade/bootstrap4/settings.py @@ -10,7 +10,7 @@ CASCADE_PLUGINS = ['accordion', 'buttons', 'card', 'carousel', 'container', 'embeds', 'icon', 'image', 'jumbotron', - 'picture', 'tabs'] + 'picture', 'tabs', 'navbar', 'lists'] if 'cms_bootstrap' in settings.INSTALLED_APPS: CASCADE_PLUGINS.append('secondary_menu') @@ -56,7 +56,9 @@ def set_defaults(config): config['plugins_with_extra_mixins'].setdefault('HorizontalRulePlugin', BootstrapUtilities( BootstrapUtilities.margins, )) - + config['plugins_with_extra_mixins'].setdefault('BootstrapJumbotronPlugin', BootstrapUtilities( + BootstrapUtilities.positions, + )) config['plugins_with_extra_fields'].setdefault('BootstrapTabSetPlugin', PluginExtraFieldsConfig( css_classes={ 'multiple': True, @@ -68,3 +70,25 @@ def set_defaults(config): ('cascade/bootstrap4/secmenu-list-group.html', _("List Group")), ('cascade/bootstrap4/secmenu-unstyled-list.html', _("Unstyled List")) ]) + config['plugins_with_extra_fields'].setdefault('BootstrapNavbarPlugin', PluginExtraFieldsConfig( + inline_styles={ + 'extra_fields:Colors':['color','background-color'], + } + )) + config['plugins_with_extra_mixins'].setdefault('BootstrapNavBrandPlugin', BootstrapUtilities( + BootstrapUtilities.background_and_color + )) + config['plugins_with_extra_mixins'].setdefault('BootstrapListsPlugin', BootstrapUtilities( + BootstrapUtilities.flex_directions, BootstrapUtilities.margins, BootstrapUtilities.paddings, BootstrapUtilities.display_propertys, BootstrapUtilities.background_and_color + )) + config['plugins_with_extra_fields'].setdefault('BootstrapListsPlugin', PluginExtraFieldsConfig( + inline_styles={ + 'extra_fields:Colors':['color','background-color'], + }, + )) + config['plugins_with_extra_mixins'].setdefault('BootstrapNavbarToogler', BootstrapUtilities( + BootstrapUtilities.background_and_color + )) + config['plugins_with_extra_mixins'].setdefault('BootstrapNavCollapsePlugin', BootstrapUtilities( + BootstrapUtilities.justify_content + )) diff --git a/cmsplugin_cascade/bootstrap4/tabs.py b/cmsplugin_cascade/bootstrap4/tabs.py index 0fd6cb485..1c14a00dc 100644 --- a/cmsplugin_cascade/bootstrap4/tabs.py +++ b/cmsplugin_cascade/bootstrap4/tabs.py @@ -11,7 +11,7 @@ from cmsplugin_cascade.plugin_base import TransparentWrapper, TransparentContainer from cmsplugin_cascade.widgets import NumberInputWidget from .plugin_base import BootstrapPluginBase - +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested class TabSetFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): num_children = IntegerField( @@ -27,6 +27,9 @@ class TabSetFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): required=False, ) + if used_compact_form: + entangled_nested(num_children, justified, data_nested='tab') + class Meta: untangled_fields = ['num_children'] entangled_fields = {'glossary': ['justified']} diff --git a/cmsplugin_cascade/bootstrap4/utils.py b/cmsplugin_cascade/bootstrap4/utils.py index eee350938..af2637dac 100644 --- a/cmsplugin_cascade/bootstrap4/utils.py +++ b/cmsplugin_cascade/bootstrap4/utils.py @@ -31,6 +31,9 @@ def get_image_tags(instance): 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) + # fallback logic + elif 'image_properties' in instance.glossary and 'width' in instance.glossary['image_properties']: + 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") @@ -52,7 +55,11 @@ def get_image_tags(instance): else: image_width = parse_responsive_length(instance.glossary['image_width_fixed']) if not image_width[0]: - image_width = (instance.image.width, image_width[1]) + if hasattr(instance,'image' ) and hasattr(instance.image,'width' ) : + image_width = (instance.image.width, image_width[1]) + # logic fallback + else: + image_width = (instance.glossary['image_properties']['width'],image_width[1] ) try: image_height = parse_responsive_length(instance.glossary['image_height']) except KeyError: @@ -99,7 +106,10 @@ def get_picture_elements(instance): 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']: + # fallback logic picture + elif 'image' in instance.glossary and 'width' in instance.glossary['image']: + aspect_ratio = compute_aspect_ratio_with_glossary(instance.glossary) + elif 'image_properties' in instance.glossary and 'width' in instance.glossary['image_properties']: aspect_ratio = compute_aspect_ratio_with_glossary(instance.glossary) else: # if accessing the image file fails or fake image fails, abort here diff --git a/cmsplugin_cascade/clipboard/admin.py b/cmsplugin_cascade/clipboard/admin.py index ae326e785..580c20ee7 100644 --- a/cmsplugin_cascade/clipboard/admin.py +++ b/cmsplugin_cascade/clipboard/admin.py @@ -6,17 +6,12 @@ from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ -from cms.api import add_plugin -from cms.models.placeholderpluginmodel import PlaceholderReference -from cms.plugin_pool import plugin_pool -from cms.utils import get_language_from_request - from jsonfield.fields import JSONField -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, CascadeClipboard +from cms.models.placeholderpluginmodel import PlaceholderReference +from cms.admin.placeholderadmin import PlaceholderAdminMixin +from cmsplugin_cascade.clipboard.utils import deserialize_to_clipboard, serialize_from_placeholder +from cmsplugin_cascade.models import CascadeClipboard, CascadeClipboardGroup class JSONAdminWidget(widgets.Textarea): def __init__(self): @@ -40,10 +35,13 @@ def render(self, name, value, attrs=None, renderer=None): _("Successfully pasted JSON data"), _("Successfully copied JSON data")) +@admin.register(CascadeClipboardGroup) +class GroupModelAdmin(PlaceholderAdminMixin, admin.ModelAdmin): + pass @admin.register(CascadeClipboard) class CascadeClipboardAdmin(admin.ModelAdmin): - fields = ('identifier', 'save_clipboard', 'restore_clipboard', 'data',) + fields = ('identifier', 'group', 'save_clipboard', 'restore_clipboard', 'data',) readonly_fields = ('save_clipboard', 'restore_clipboard',) formfield_overrides = { JSONField: {'widget': JSONAdminWidget}, @@ -64,98 +62,18 @@ def restore_clipboard(self, obj): restore_clipboard.short_description = _("To CMS Clipboard") def save_model(self, request, obj, form, change): - language = get_language_from_request(request) if request.POST.get('save_clipboard'): - obj.data = self._serialize_from_clipboard(language) + 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 CMS clipboard has been persisted in the database.")) + messages.add_message(request, messages.INFO, _("The CMS clipboard has been persisted into the database.")) 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 CMS clipboard.")) super().save_model(request, obj, form, change) if request.POST.get('restore_clipboard'): - self._deserialize_to_clipboard(request, obj.data) - - - def _serialize_from_clipboard(self, language): - """ - 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(self.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': []} - ref = PlaceholderReference.objects.last() - if ref: - clipboard = ref.placeholder_ref - plugin_qs = clipboard.cmsplugin_set.all() - populate_data(None, data['plugins']) - return data - - def _deserialize_to_clipboard(self, request, data): - """ - Restore clipboard 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) - - 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()[0] - inst.placeholder_ref.get_plugins().delete() - plugins_from_data(root_plugin.placeholder_ref, None, data['plugins']) + deserialize_to_clipboard(request, obj.data) diff --git a/cmsplugin_cascade/clipboard/cms_plugins.py b/cmsplugin_cascade/clipboard/cms_plugins.py new file mode 100644 index 000000000..42f7bfa6b --- /dev/null +++ b/cmsplugin_cascade/clipboard/cms_plugins.py @@ -0,0 +1,334 @@ +import json +from django.conf.urls import url +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, ModelMultipleChoiceField, ChoiceField, MultipleChoiceField +from django.shortcuts import render +from django.template.response import TemplateResponse +from django.urls import reverse +from django.utils.http import urlencode +from django.utils.translation import ugettext_lazy as _ + +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 cms.utils import get_language_from_request +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, CascadeClipboardGroup +from cmsplugin_cascade.clipboard.forms import ClipboardBaseForm +from django.forms import widgets +from django.contrib.admin.widgets import RelatedFieldWidgetWrapper +from django.conf import settings + + +class ClipboardWidget(widgets.Select): + #template_name = 'django/forms/widgets/select.html' + template_name = 'cascade/admin/widgets/clipboard.html' + + def get_context(self, name, value, attrs): + context = super(ClipboardWidget, self).get_context(name, value, attrs) + context['widget']['optgroups'] = self.optgroups(name, context['widget']['value'], attrs) + groups=list(CascadeClipboardGroup.objects.all().exclude( name='Clipboard Home').values_list('name',flat=True)) + context['groups_exclude_home'] = groups + context['qs_clipboards'] = CascadeClipboard.objects.all() + context['main_scss'] = settings.CMSPLUGIN_CASCADE['fallback']['path_main_scss'] + return context + + +class CascadeClipboardPlugin(CMSPluginBase): + system = True + render_plugin = False + change_form_template = 'admin/cms/page/plugin/change_form.html' + + def get_plugin_urls(self): + urlpatterns = [ + url(r'^export-plugins/$', self.export_plugins_view, name='export_clipboard_plugins'), + url(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), + }) + return [ + PluginMenuItem( + _("Export from 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': True, + 'change': True, + 'save_as': True, + 'has_add_permission': True, + 'has_change_permission': True, + 'can_change_related':True, + 'can_add_related':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, *args, **kwargs): + # TODO: check for permissions + + + view_breakdown = request.session.get('view_breakdown', "lg") + + placeholder_ref_id = None + if request.GET.get('placeholder'): + placeholder_ref_id = request.GET.get('placeholder') + queryset=CascadeClipboard.objects.all().prefetch_related('group') + clipboards_groupby={} + + def treegroup( groups, index2): + groups_clipboard=list(groups.group.values_list('name', flat=True)) + if len(groups_clipboard) >= 1: + for index, key in enumerate(groups_clipboard, start=1): + clipboards_groupby.setdefault(key, []) + clipboards_groupby[key].append(( groups.identifier ,groups.identifier,)) + else: + clipboards_groupby.setdefault('ungroup', []) + clipboards_groupby['ungroup'].append(( groups.identifier ,groups.identifier,)) + + [treegroup( groups, index) for index, groups in enumerate(queryset , start=1)] + + if not 'Clipboard Home' in clipboards_groupby: + identifier = 'Demo' + group ='Clipboard Home' + + # data_demo = self.populate_static_json("cascade/admin/clipboards/demo_carousel-plugin.json") + # self.populate_db_group_clipboards( clipboards_groupby, identifier, group, data_demo) + + # folder to group and file to group. + data_folders = self.populate_static_folderGroup_json('cascade/admin/clipboards/') + if data_folders: + self.populate_db_data_clipboards( data_folders, identifier, group) + + # Clipboard home + data_demo = self.populate_static_json("cascade/admin/clipboards/demo/demo_carousel-plugin.json") + self.populate_db_group_clipboards( clipboards_groupby, identifier, group, data_demo) + + + CHOICES=(list(clipboards_groupby.items(),)) + ff=_("Import from Clipboard") + + if request.GET.get('group'): + req_parameter_group = request.GET.get('group') + title = ": {}".format(req_parameter_group) + else: + req_parameter_group = "Clipboard Home" + title = _("Import to Clipboard") + + # if empty clipboards but has group do empty + if not req_parameter_group in clipboards_groupby: + clipboards_groupby[req_parameter_group] = '' + + if 'ungroup' in clipboards_groupby : + len_ungroup = len(clipboards_groupby[req_parameter_group]) + else: + len_ungroup = 0 + + CHOICES=clipboards_groupby[req_parameter_group] + language= get_language_from_request(request) + if request.method == 'GET': + Form = type('ClipboardImportForm', (ClipboardBaseForm,), { + 'clipboard':ChoiceField( + choices=CHOICES, + label=_("Select Clipboard"), + required=False, + widget=ClipboardWidget(attrs={"placeholder_ref_id": placeholder_ref_id, "language": language, 'count_target':len_ungroup ,'view_breakdown':view_breakdown }), + ), + 'title': title, + }) + + Form.Media = type("Media",(), {'css' : { 'all': [ ''] }}) + form = Form(request.GET) + assert form.is_valid() + elif request.method == 'POST': + Form = type('ClipboardImportForm', (ClipboardBaseForm,), { + 'clipboard': ChoiceField( + choices=CHOICES, + label=_("Select Clipboard"), + widget=ClipboardWidget(), + ), + '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) + if not hasattr(cascade_clipboard, 'data'): + deserialize_to_clipboard(request, CascadeClipboard.objects.get(identifier=cascade_clipboard).data) + else: + deserialize_to_clipboard(request,cascade_clipboard.data) + + # detach plugins from clipboard and reattach them to current placeholder + cb_placeholder_plugin = request.toolbar.clipboard.cmsplugin_set.first() + cb_placeholder_instance, _ = cb_placeholder_plugin.get_plugin_instance() + + # bug if the Clipboard Placeholder has alias 'AliasPluginModel', object, it has no attribute 'placeholder_ref', + # possible need request.toolbar.clipboard.clear() , add .placeholder_ref + 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 + 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__'] + 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 + + qs_clipboards=CascadeClipboardGroup.objects.all() + if not'Clipboard Home' in list(qs_clipboards.values_list( 'name' , flat=True)): + qs_clipboards.get_or_create(name="Clipboard Home") + + title = _("Export to Clipboard") + if request.method == 'GET': + Form = type('ClipboardExportForm', (ClipboardBaseForm,), { + 'identifier': CharField(required=False), + 'title': title, + 'group' : ModelMultipleChoiceField( + queryset=qs_clipboards, + required=False, + ), + }) + form = Form(request.GET) + form.fields['group'].widget = RelatedFieldWidgetWrapper( + form.fields['group'].widget,CascadeClipboard.group.rel, + default_admin_site, can_change_related=True) + + assert form.is_valid() + elif request.method == 'POST': + Form = type('ClipboardExportForm', (ClipboardBaseForm,), { + 'identifier': CharField(), + 'title': title, + 'group' : ModelMultipleChoiceField( + queryset=qs_clipboards, + required=False, + ), + }) + form = Form(request.POST) + form.fields['group'].widget = RelatedFieldWidgetWrapper( + form.fields['group'].widget,CascadeClipboard.group.rel, + default_admin_site, can_change_related=True) + + 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'] + group = form.cleaned_data['group'] + data = serialize_from_placeholder(placeholder) + cascade_clipboard = CascadeClipboard.objects.create( + identifier=identifier, + data=data, + ) + cascade_clipboard.group.set(group) + return render(request, 'cascade/admin/clipboard_close_frame.html', {}) + + + def populate_db_group_clipboards(self, clipboards_groupby, identifier, group, data_clipboard): + clipboards_groupby[ group] = [( identifier, identifier)] + clipboard_home = CascadeClipboardGroup.objects.get_or_create(name=group) + cascade_clipboard = CascadeClipboard.objects.get_or_create( + identifier=identifier, + data=data_clipboard, + ) + cascade_clipboard[0].group.set([clipboard_home[0]]) + + def populate_static_json(self, relative_path_filename): + import os, io, json + from django.contrib.staticfiles import finders + path = finders.find(relative_path_filename) + with io.open(path, 'r') as fh: + config_data = json.load(fh) + return config_data + + def populate_db_data_clipboards(self,data, identifier, group_name ): + for group_name , values in data.items(): + if len(values) >= 1: + for value in values: + identifier = value.split('/')[-1].replace('.json','') + data_clipboard = self.populate_static_json(value) + self.populate_db_group_clipboards(data, identifier, group_name, data_clipboard) + + + def populate_static_folderGroup_json(self, relative_path_folder): + import os, io, json + import pathlib + from django.contrib.staticfiles import finders + input_path = finders.find(relative_path_folder) + data = {} + if input_path: + list_folders_top=next(os.walk(input_path))[1] + for n, group_folder in enumerate(list_folders_top, 1): + clipboards_folder=[] + list_subfolder_path=os.path.join(input_path, group_folder) + files_path=list(pathlib.Path(list_subfolder_path).glob('**/*.json')) + for path in files_path: + clipboards_folder.append( str(pathlib.Path(relative_path_folder).joinpath(path.relative_to(input_path)))) + data.update({ group_folder : clipboards_folder}) + return data + + + +plugin_pool.register_plugin(CascadeClipboardPlugin) diff --git a/cmsplugin_cascade/clipboard/cms_toolbars.py b/cmsplugin_cascade/clipboard/cms_toolbars.py new file mode 100644 index 000000000..02f05f1ee --- /dev/null +++ b/cmsplugin_cascade/clipboard/cms_toolbars.py @@ -0,0 +1,10 @@ +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'] + } diff --git a/cmsplugin_cascade/clipboard/forms.py b/cmsplugin_cascade/clipboard/forms.py new file mode 100644 index 000000000..3cfe19d3d --- /dev/null +++ b/cmsplugin_cascade/clipboard/forms.py @@ -0,0 +1,28 @@ +from django import forms +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_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 diff --git a/cmsplugin_cascade/clipboard/utils.py b/cmsplugin_cascade/clipboard/utils.py new file mode 100644 index 000000000..d74783050 --- /dev/null +++ b/cmsplugin_cascade/clipboard/utils.py @@ -0,0 +1,92 @@ +from django.contrib.admin import site as default_admin_site +from django.contrib import messages + +from cms.api import add_plugin +from cms.models.placeholderpluginmodel import PlaceholderReference +from cms.plugin_pool import plugin_pool +from cms.utils import get_language_from_request + +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) + + 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']) diff --git a/cmsplugin_cascade/extra_fields/mixins.py b/cmsplugin_cascade/extra_fields/mixins.py index 1fa95f50f..35acdc5cf 100644 --- a/cmsplugin_cascade/extra_fields/mixins.py +++ b/cmsplugin_cascade/extra_fields/mixins.py @@ -1,3 +1,4 @@ + from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ObjectDoesNotExist from django.forms import MediaDefiningClass, widgets @@ -7,7 +8,8 @@ from entangled.forms import EntangledModelFormMixin from cmsplugin_cascade import app_settings from cmsplugin_cascade.fields import SizeField - +from entangled.forms import EntangledModelFormMixin +from cmsplugin_cascade.helpers import entangled_nested, used_compact_form class ExtraFieldsMixin(metaclass=MediaDefiningClass): """ @@ -30,6 +32,21 @@ def get_form(self, request, obj=None, **kwargs): except ObjectDoesNotExist: extra_fields = app_settings.CMSPLUGIN_CASCADE['plugins_with_extra_fields'].get(clsname) + if hasattr(extra_fields, 'id') and app_settings.CMSPLUGIN_CASCADE['merge_extra_fields'] : + settings_extra_fields = app_settings.CMSPLUGIN_CASCADE['plugins_with_extra_fields'] + if clsname in settings_extra_fields.keys(): + for key_style, value_style in settings_extra_fields[clsname].inline_styles.items(): + extra_fields.inline_styles.update({key_style:extra_fields.inline_styles[key_style] + value_style}) + for key_css_classes, value_css_classes in settings_extra_fields[clsname].css_classes.items(): + if value_css_classes == '': + value_css_classes = extra_fields.css_classes[key_css_classes] + elif type(value_css_classes) == list: + list_extra_fields = extra_fields.css_classes[key_css_classes].replace(' ', '').split(",") + list_css_classes = list(dict.fromkeys(list_extra_fields + value_css_classes)) + value_css_classes = ','.join(list_css_classes) + extra_fields.css_classes.update({ key_css_classes:value_css_classes}) + extra_fields.save() + if isinstance(extra_fields, (PluginExtraFields, PluginExtraFieldsConfig)): form_fields = {} @@ -62,7 +79,8 @@ def get_form(self, request, obj=None, **kwargs): required=False, help_text=_("Customized CSS class to be added to this element."), ) - + if used_compact_form: + entangled_nested(form_fields['extra_css_classes'], data_nested="custom_css_classes") # 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)) @@ -77,7 +95,10 @@ def get_form(self, request, obj=None, **kwargs): } 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) + field = Field(**field_kwargs) + if used_compact_form: + entangled_nested(field, data_nested=style.split(':')[0]) + form_fields[key] = field # extend the form with some extra fields base_form = kwargs.pop('form', self.form) diff --git a/cmsplugin_cascade/fields.py b/cmsplugin_cascade/fields.py index 338325ebd..122989d8c 100644 --- a/cmsplugin_cascade/fields.py +++ b/cmsplugin_cascade/fields.py @@ -165,9 +165,18 @@ def compress(self, data_list): @classmethod def css_value(self, values): - return values[0] + color, inherit_color = values + return color if not inherit_color else '' +class ColorFieldExtra(ColorField): + """ + ColorFieldExtra have default inherit_color=True. + """ + def __init__(self, *args, **kwargs): + kwargs['inherit_color'] = True + super().__init__(*args, **kwargs) + @deconstructible class SizeUnitValidator(): allowed_units = [] diff --git a/cmsplugin_cascade/forms.py b/cmsplugin_cascade/forms.py index 5681dac69..94078bd16 100644 --- a/cmsplugin_cascade/forms.py +++ b/cmsplugin_cascade/forms.py @@ -15,3 +15,20 @@ def __init__(self, *args, **kwargs): initial = {'num_children': instance.get_num_children()} kwargs.update(initial=initial) super().__init__(*args, **kwargs) + + +class ManageNestedFormMixin(object): + """ + Classes derived from ``CascadePluginBase`` can optionally add this mixin class to their form, + offering initial data (instance.glossary) of form nested. + """ + def __init__(self, *args, **kwargs): + instance = kwargs.get('instance') + if instance: + for field_name, field in self.base_fields.items(): + if len(field_name.split('.')) == 2: + tenant_nested = field_name.split('.')[0] + field_nested = field_name.split('.')[1] + if tenant_nested in instance.glossary and field_nested in instance.glossary[tenant_nested]: + field.initial = instance.glossary[tenant_nested][field_nested] + super().__init__(*args, **kwargs) diff --git a/cmsplugin_cascade/generic/mixins_html_attrs.py b/cmsplugin_cascade/generic/mixins_html_attrs.py new file mode 100644 index 000000000..c40b1b6e9 --- /dev/null +++ b/cmsplugin_cascade/generic/mixins_html_attrs.py @@ -0,0 +1,120 @@ +from django.forms.fields import ChoiceField +from django.utils.text import format_lazy +from django.utils.translation import ugettext_lazy as _ +from entangled.forms import EntangledModelFormMixin +from cmsplugin_cascade.utils import CascadeUtilitiesMixin +from cmsplugin_cascade.bootstrap4.grid import Breakpoint +from cmsplugin_cascade import app_settings +from cmsplugin_cascade.helpers import used_compact_form + +def get_widget_choices(widget_choices): + return widget_choices + +class GenericUtilities(type): + """ + Factory for building a class ``GenericUtilitiesMixin``. This class then is used as a mixin to + all sorts of generic plugins. Various 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'] = { + 'BootstrapPlugin': GenericUtilities( + GenericUtilities.scroll_animate, + … + ), + … + } + ``` + + The class ``GenericUtilities`` 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. + Specficed with attritbute in property methods 'attrs_type' with two possible values 'css_classes' or 'html_data_attrs' + Html data attribute need some time anchors of current page. + form_fields has reserved string '#anchors' used with choices='#anchors', after the form request anchor of element_ids + are disponible in choicesfields. + """ + def __new__(cls, *args): + form_fields = {} + form_fields_by_property_name = {} + form_fields_by_attr_type = {} + fields_choices_anchors = [] + + for arg in args: + if isinstance(arg, property): + property_fields=arg.fget(cls) + form_subfields = property_fields['form_fields'] + attrs_type = property_fields['attrs_type'] + property_name = property_fields['property_name'] + + form_fields.update(form_subfields) + form_fields_by_property_name[property_name]= property_fields['form_fields'] + + form_fields_by_attr_type.setdefault(attrs_type, []) + form_fields_by_attr_type[attrs_type ].extend(property_fields['form_fields'].keys()) + + if 'anchors_fields' in property_fields: + fields_choices_anchors.extend(property_fields['anchors_fields']) + + if used_compact_form: + for property_name , field in form_fields_by_property_name.items(): + entangled_nested(field, data_nested=property_name) + + class Meta: + entangled_fields = {'glossary': list(form_fields) } + + utility_form_mixin = type('UtilitiesFormMixin', (EntangledModelFormMixin,), dict(form_fields, Meta=Meta) ) + return type('HtmlAttrsUtilitiesMixin', (CascadeUtilitiesMixin,), {'utility_form_mixin': utility_form_mixin, + 'attr_type': form_fields_by_attr_type , 'fields_with_choices_anchors': fields_choices_anchors }) + + @property + def scroll_animate(cls): + form_fields = {} + attrs_type = 'html_data_attrs' + property_name = 'scroll_animate' + + choices_data_sal = [ + ('inherit', _("inherit")), + ('fade', _("fade")), + ('slide-up', _("slide-up")), + ('slide-down', _("slide-down")), + ('slide-left', _("slide-left")), + ('slide-right', _("slide-right")), + ('zoom-in', _("zoom-in")), + ('zoom-out', _("zoom-out")), + ('flip-up', _("flip-up")), + ('flip-down', _("flip-down")), + ('zoom-out', _("zoom-out")), + ('flip-up', _("flip-up")), + ('flip-right', _("flip-right")), + ] + + choices_data_sal_delay= list((c, c) for c in ["inherit"] + [ i for i in range(0, 2100, 100)]) + choices_data_sal_easing= [ + ('ease', _("ease")), + ('ease-in-out-back', _("ease-in-out-back")), + ('ease-out-back', _("ease-out-back")), + ('ease-in-out-sine', _("ease-in-out-sine")), + ('ease-in-quad', _("ease-in-quad")), + ] + form_fields['data-sal'] = ChoiceField( + label=_("Scroll effects"), + choices=choices_data_sal, + required=False, + initial='', + ) + form_fields['data-sal-delay'] = ChoiceField( + label=_("Delay effect"), + choices= choices_data_sal_delay, + required=False, + initial='', + help_text='Delay in milliseconde', + ) + attrs={'data_entangled':'Scroll_animate'} + form_fields['data-sal-delay'].widget.attrs = {**attrs } + form_fields['data-sal-easing'] = ChoiceField( + label=_("Animation type"), + choices=choices_data_sal_easing, + required=False, + initial='', + ) + return { 'form_fields':form_fields, 'attrs_type': attrs_type, 'property_name':property_name } diff --git a/cmsplugin_cascade/generic/text_image.py b/cmsplugin_cascade/generic/text_image.py index 71dc64a5a..6e412409d 100644 --- a/cmsplugin_cascade/generic/text_image.py +++ b/cmsplugin_cascade/generic/text_image.py @@ -8,7 +8,7 @@ from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin from cmsplugin_cascade.link.plugin_base import LinkElementMixin from cmsplugin_cascade.utils import compute_aspect_ratio - +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested class TextImageFormMixin(ImageFormMixin): RESIZE_OPTIONS = [ @@ -49,6 +49,9 @@ class TextImageFormMixin(ImageFormMixin): initial='', ) + if used_compact_form: + entangled_nested(image_width, image_height, resize_options, alignement, data_nested='text_image') + class Meta: entangled_fields = {'glossary': ['image_width', 'image_height', 'resize_options', 'alignement']} diff --git a/cmsplugin_cascade/helpers.py b/cmsplugin_cascade/helpers.py new file mode 100644 index 000000000..1105c0beb --- /dev/null +++ b/cmsplugin_cascade/helpers.py @@ -0,0 +1,105 @@ +from os import environ +from django.utils.translation import ugettext_lazy as _ +from django.contrib.admin.utils import flatten_fieldsets +from django.forms import widgets +from cmsplugin_cascade import app_settings +from django.forms import Media +from collections import OrderedDict + +from django.forms.widgets import media_property + +used_compact_form = True if environ.get('COMPACT_FORM', False) == 'True' else False + +traductions_keys_to_title = { + 'background_and_color': _( "Background and color" ), + 'scroll_animate': _( "Scroll Animate" ), + 'custom_css_classes': _( "Custom css classes" ), + } + +def fieldset_by_widget_attr( form, attr_data_name, cls_media, traductions=traductions_keys_to_title , change_form_template=None): + + """Filter and classed fields with or not group attribute 'data_nested' and create fieldset.""" + nested = {} + css_appended = [ 'cascade/css/admin/compact_forms/main_compact_form.css', 'cascade/css/admin/cascade_box.css' ] + fieldsets = () + for key, field in form.declared_fields.items(): + if 'data_nested' in field.widget.attrs : + data_entangled_value = field.widget.attrs[attr_data_name] + if data_entangled_value == 'background_and_color': + css_appended.append('cascade/css/admin/compact_forms/bootstrap4-colors.css') + + nested.setdefault(data_entangled_value,[]) + if len(key) > 1: + nested[data_entangled_value].append(key) + else: + nested[data_entangled_value].extend(key) + + if hasattr(cls_media, 'css'): + if not css_appended[0] in cls_media.css['all']: + cls_media.css['all'].extend(css_appended) + else: + cls_media.css = { + 'all': css_appended + } + + for key_title, fields_lists_str in nested.items(): + + if key_title in traductions: + key_title_trad = traductions[key_title] + else: + key_title_trad = key_title + + icon = '{0}-title'.format(key_title) + + if 'link_type' in fields_lists_str or 'icon_font' in fields_lists_str or 'button' in fields_lists_str: + fieldsets +=( None, {'classes': ('cascade_box',),'fields':((fields_lists_str),), 'description': + '
{1}
\ +
'.format(icon , key_title )}), + if key_title in ['background_and_color', 'column', 'offset', 'reorder',\ + 'responsive', 'floats', 'paddings','margins', 'buttons', 'vertical_margins', 'container']: + fieldsets +=( None, { 'classes': ['cascade_box', 'nested'], 'fields':((fields_lists_str),), + 'description': '
{1}\ +
'.format(icon, key_title_trad )}), + # else: + if not key_title in ['background_and_color', 'column', 'offset', 'reorder',\ + 'responsive', 'floats', 'paddings','margins', 'buttons', 'vertical_margins', 'container']: + fieldsets +=( None ,{'classes': ['cascade_box_classic',], 'fields':fields_lists_str }), + + extra_fields = OrderedDict.fromkeys(x for x in list(form.declared_fields.keys()) if x not in flatten_fieldsets(fieldsets)) + if not nested: + fieldsets +=(None, {'fields':list(extra_fields)}), + else: + # fieldsets +=('extra_fields',{'fields':list(extra_fields)}), + fieldsets +=(None ,{'fields':list(extra_fields)}), + return fieldsets, cls_media + + +def apply_widgets_tpl( field,template_key): + if template_key == 'select_icon': + field.widget.template_name = 'cascade/admin/widgets/select_icon.html' + if template_key == 'button_type': + field.widget.template_name = 'cascade/admin/compact_forms/widgets/select_icon_button_types.html' + if template_key == 'width': + field.widget.template_name = 'cascade/admin/compact_forms/widgets/select_icon_button_types.html' + if template_key == 'column' or template_key == 'paddings' or template_key == 'margins' or template_key == 'floats' or template_key == 'vertical_margins' : + field.widget.template_name = 'cascade/admin/compact_forms/widgets/select_icon_columns.html' + if template_key == 'background_and_color': + field.widget.template_name = 'cascade/admin/compact_forms/widgets/select_icon_colors.html' + + +def entangled_nested(*fields, data_nested=None,template_key=None): + """ + The Fields are classed by groups key with widget attribute 'data_nested' and set widget template name. + Used in Compact form mode. + """ + for index, field in enumerate(fields): + if isinstance(field, dict): + for index_sub, field in enumerate(field.values()): + field.widget.attrs['data_nested'] = data_nested + field.widget.attrs['pk'] = index + apply_widgets_tpl(field, template_key) + index += 1 + else: + field.widget.attrs['data_nested'] = data_nested + field.widget.attrs['pk'] = index + apply_widgets_tpl(field, template_key) diff --git a/cmsplugin_cascade/icon/forms.py b/cmsplugin_cascade/icon/forms.py index 8d1eaddd7..26981425d 100644 --- a/cmsplugin_cascade/icon/forms.py +++ b/cmsplugin_cascade/icon/forms.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from cmsplugin_cascade.models import IconFont from entangled.forms import EntangledModelFormMixin - +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested def get_default_icon_font(): try: @@ -23,6 +23,10 @@ class IconFormMixin(EntangledModelFormMixin): label=_("Select Symbol"), ) + if used_compact_form : + entangled_nested(icon_font,symbol, data_nested='icon') + + class Meta: entangled_fields = {'glossary': ['icon_font', 'symbol']} diff --git a/cmsplugin_cascade/icon/settings.py b/cmsplugin_cascade/icon/settings.py new file mode 100644 index 000000000..55f57f425 --- /dev/null +++ b/cmsplugin_cascade/icon/settings.py @@ -0,0 +1 @@ +CASCADE_PLUGINS = ['simpleicon', 'texticon'] diff --git a/cmsplugin_cascade/icon/simpleicon.py b/cmsplugin_cascade/icon/simpleicon.py new file mode 100644 index 000000000..9b3b56a95 --- /dev/null +++ b/cmsplugin_cascade/icon/simpleicon.py @@ -0,0 +1,24 @@ +from django.utils.translation import ugettext_lazy as _ + +from cms.plugin_pool import plugin_pool +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 SimpleIconPlugin(IconPluginMixin, LinkPluginBase): + name = _("Simple Icon") + parent_classes = None + require_parent = False + allow_children = False + render_template = 'cascade/plugins/simpleicon.html' + form = type('SimpleIconForm', (LinkFormMixin, IconFormMixin), {'require_link': False}) + model_mixins = (LinkElementMixin,) + ring_plugin = 'IconPlugin' + + class Media: + js = ['admin/js/jquery.init.js', 'cascade/js/admin/iconplugin.js'] + +plugin_pool.register_plugin(SimpleIconPlugin) + diff --git a/cmsplugin_cascade/icon/cms_plugins.py b/cmsplugin_cascade/icon/texticon.py similarity index 67% rename from cmsplugin_cascade/icon/cms_plugins.py rename to cmsplugin_cascade/icon/texticon.py index 89961c7c3..1e29ce57a 100644 --- a/cmsplugin_cascade/icon/cms_plugins.py +++ b/cmsplugin_cascade/icon/texticon.py @@ -1,25 +1,23 @@ from django.utils.translation import ugettext_lazy as _ + from cms.plugin_pool import plugin_pool 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 +from django.forms.fields import CharField +from entangled.forms import EntangledModelFormMixin -class SimpleIconPlugin(IconPluginMixin, LinkPluginBase): - name = _("Simple Icon") - parent_classes = None - require_parent = False - allow_children = False - render_template = 'cascade/plugins/simpleicon.html' - form = type('SimpleIconForm', (LinkFormMixin, IconFormMixin), {'require_link': False}) - model_mixins = (LinkElementMixin,) - ring_plugin = 'IconPlugin' - - class Media: - js = ['admin/js/jquery.init.js', 'cascade/js/admin/iconplugin.js'] +class SimpleIconFormMixin(EntangledModelFormMixin): + content = CharField( + label=_('Content'), + required=False, + help_text=_("Content inside SimpleIcon"), + ) -plugin_pool.register_plugin(SimpleIconPlugin) + class Meta: + entangled_fields = {'glossary': ['content']} class TextIconPlugin(IconPluginMixin, LinkPluginBase): diff --git a/cmsplugin_cascade/image.py b/cmsplugin_cascade/image.py index 6142511d8..9cb9cb24b 100644 --- a/cmsplugin_cascade/image.py +++ b/cmsplugin_cascade/image.py @@ -59,3 +59,4 @@ def post_copy(self, old_instance, new_old_ziplist): # by saving this model after the full tree has been copied, ``ImagePlugin.sanitize_model()`` # is invoked a second time with the now complete information of all column siblings. self.save(sanitize_only=True) + diff --git a/cmsplugin_cascade/link/cms_plugins.py b/cmsplugin_cascade/link/cms_plugins.py index da1989e55..16d43b552 100644 --- a/cmsplugin_cascade/link/cms_plugins.py +++ b/cmsplugin_cascade/link/cms_plugins.py @@ -18,6 +18,18 @@ class TextLinkPlugin(LinkPluginBase): class Media: js = ['admin/js/jquery.init.js', 'cascade/js/admin/textlinkplugin.js'] + + @classmethod + def get_css_classes(cls, obj): + css_classes = cls.super(TextLinkPlugin, cls).get_css_classes(obj) + if hasattr(obj.parent.parent, 'plugin_type' ) and obj.parent.parent.plugin_type == 'BootstrapListsPlugin': + css_classes.insert(0,'nav-link navbar-text') + # strides pass + if hasattr(obj.parent.parent, 'plugin'): + css_classes.insert(0,'nav-link navbar-text') + return css_classes + + @classmethod def get_identifier(cls, obj): return mark_safe(obj.glossary.get('link_content', '')) diff --git a/cmsplugin_cascade/link/forms.py b/cmsplugin_cascade/link/forms.py index 17e2adfb4..d07f33a60 100644 --- a/cmsplugin_cascade/link/forms.py +++ b/cmsplugin_cascade/link/forms.py @@ -9,12 +9,22 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from django_select2.forms import HeavySelect2Widget + from cms.utils import get_current_site from cms.models import Page from entangled.forms import EntangledModelFormMixin, get_related_object from filer.models.filemodels import File as FilerFileModel from filer.fields.file import AdminFileWidget, FilerFileField + +from cmsplugin_cascade.helpers import used_compact_form, entangled_nested + + +try: + from phonenumber_field.formfields import PhoneNumberField +except ImportError: + PhoneNumberField = None + def format_page_link(title, path): html = format_html("{} ({})", mark_safe(title), path) @@ -81,6 +91,8 @@ class LinkForm(EntangledModelFormMixin): ('exturl', _("External URL")), ('email', _("Mail To")), ] + if PhoneNumberField: + LINK_TYPE_CHOICES.append(('phonenumber', _("Phone number"))) link_type = fields.ChoiceField( label=_("Link"), @@ -119,6 +131,13 @@ class LinkForm(EntangledModelFormMixin): help_text=_("Open Email program with this address"), ) + if PhoneNumberField: + phone_number = PhoneNumberField( + required=False, + label=_("Phone Number"), + help_text=_("International phone number, ex. +1 212 555 2368."), + ) + link_target = fields.ChoiceField( choices=[ ('', _("Same Window")), @@ -138,9 +157,15 @@ class LinkForm(EntangledModelFormMixin): help_text=_("Link's Title"), ) + if used_compact_form: + entangled_nested(link_type, cms_page, section, download_file, ext_url, mail_to, + link_target, link_title, data_nested='link') + class Meta: entangled_fields = {'glossary': ['link_type', 'cms_page', 'section', 'download_file', 'ext_url', 'mail_to', 'link_target', 'link_title']} + if PhoneNumberField: + entangled_fields['glossary'].append('phone_number') def __init__(self, *args, **kwargs): link_type_choices = [] @@ -197,11 +222,16 @@ def clean(self): mail_to = cleaned_data.get('mail_to') if mail_to: if not re.match(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)', mail_to): - error = ValidationError(_("'{email}' is not a valid email address.").format(email=mail_to)) + msg = _("'{email}' is not a valid email address.") + error = ValidationError(msg.format(email=mail_to)) else: error = ValidationError(_("No email address provided.")) if error: self.add_error('mail_to', error) + elif link_type == 'phonenumber': + phone_number = cleaned_data.get('phone_number') + if phone_number: + cleaned_data['phone_number'] = str(phone_number) if error: raise error return cleaned_data diff --git a/cmsplugin_cascade/link/plugin_base.py b/cmsplugin_cascade/link/plugin_base.py index 9de1b0e42..bb8739bc0 100644 --- a/cmsplugin_cascade/link/plugin_base.py +++ b/cmsplugin_cascade/link/plugin_base.py @@ -24,6 +24,8 @@ def get_link(cls, obj): return '{ext_url}'.format(**obj.glossary) if linktype == 'email': return 'mailto:{mail_to}'.format(**obj.glossary) + if linktype == 'phonenumber': + return 'tel:{phone_number}'.format(**obj.glossary) # otherwise resolve by model if linktype == 'cmspage': diff --git a/cmsplugin_cascade/migrations/0027_version_1.py b/cmsplugin_cascade/migrations/0027_version_1.py index a21f06754..31d38fc58 100644 --- a/cmsplugin_cascade/migrations/0027_version_1.py +++ b/cmsplugin_cascade/migrations/0027_version_1.py @@ -58,7 +58,7 @@ def migrate_image(glossary): }) if 'width' in image and 'height' in image and 'exif_orientation' in image: glossary.update({ - '_image_properties': {'width': image['width'], 'height': image['height'], + 'image_properties': {'width': image['width'], 'height': image['height'], 'exif_orientation': image['exif_orientation']}, }) return True diff --git a/cmsplugin_cascade/migrations/0028_auto_20200218_1514.py b/cmsplugin_cascade/migrations/0028_auto_20200218_1514.py new file mode 100644 index 000000000..42ff05426 --- /dev/null +++ b/cmsplugin_cascade/migrations/0028_auto_20200218_1514.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.10 on 2020-02-18 21:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cmsplugin_cascade', '0027_version_1'), + ] + + operations = [ + migrations.CreateModel( + name='CascadeClipboardGroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ], + ), + migrations.AddField( + model_name='cascadeclipboard', + name='group', + field=models.ManyToManyField(blank=True, to='cmsplugin_cascade.CascadeClipboardGroup'), + ), + ] diff --git a/cmsplugin_cascade/models.py b/cmsplugin_cascade/models.py index 8a88a4a72..65f362183 100644 --- a/cmsplugin_cascade/models.py +++ b/cmsplugin_cascade/models.py @@ -34,7 +34,11 @@ class SharedGlossary(models.Model): unique=True, ) - glossary = JSONField(null=True, blank=True, default={}) + glossary = JSONField( + null=True, + blank=True, + default={}, + ) class Meta: unique_together = ['plugin_type', 'identifier'] @@ -106,7 +110,10 @@ class InlineCascadeElement(models.Model): on_delete=models.CASCADE, ) - glossary = JSONField(blank=True, default={}) + glossary = JSONField( + blank=True, + default={}, + ) class Meta: db_table = 'cmsplugin_cascade_inline' @@ -218,6 +225,11 @@ class Meta: managed = False # it's a dummy model db_table = None +class CascadeClipboardGroup(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return str(self.name) class CascadeClipboard(models.Model): """ @@ -229,6 +241,8 @@ class CascadeClipboard(models.Model): unique=True, ) + group = models.ManyToManyField(CascadeClipboardGroup,blank=True) + data = JSONField( null=True, blank=True, diff --git a/cmsplugin_cascade/plugin_base.py b/cmsplugin_cascade/plugin_base.py index 0150758bd..91598f8a1 100644 --- a/cmsplugin_cascade/plugin_base.py +++ b/cmsplugin_cascade/plugin_base.py @@ -18,11 +18,23 @@ from .hide_plugins import HidePluginMixin from .render_template import RenderTemplateMixin from .utils import remove_duplicates +from .helpers import fieldset_by_widget_attr +from cmsplugin_cascade.helpers import used_compact_form mark_safe_lazy = lazy(mark_safe, str) fake_proxy_models = {} +def form_initial_data_nested(form, initial_data): + #truc = kwargs.get('truc', False) + if initial_data: + for field_name, field in form.base_fields.items(): + if len(field_name.split('.')) == 2: + tenant_nested = field_name.split('.')[0] + field_nested = field_name.split('.')[1] + field.initial = initial_data[tenant_nested ][field_nested] + + def create_proxy_model(name, model_mixins, base_model, attrs=None, module=None): """ @@ -69,6 +81,24 @@ class CascadePluginMixinBase(metaclass=CascadePluginMixinMetaclass): """ +def __getitem__(cls , name): + """Return a BoundField with the given name.""" + try: + field = self.fields[name] + except KeyError: + raise KeyError( + "Key '%s' not found in '%s'. Choices are: %s." % ( + name, + self.__class__.__name__, + ', '.join(sorted(self.fields)), + ) + ) + if name not in self._bound_fields_cache: + self._bound_fields_cache[name] = field.get_bound_field(self, name) + return self._bound_fields_cache[name] + + + class CascadePluginBaseMetaclass(CascadePluginMixinMetaclass, CMSPluginBaseMetaclass): """ All plugins from djangocms-cascade can be instantiated in different ways. In order to allow this @@ -324,7 +354,13 @@ def get_form(self, request, obj=None, **kwargs): bases = (CascadeFormMixin,) + bases if not issubclass(form, ModelForm): bases += (ModelForm,) - kwargs['form'] = type(form.__name__, bases, {}) + + + form = type(form.__name__, bases, {}) + if used_compact_form: + self.fieldsets, self.Media = fieldset_by_widget_attr(form ,'data_nested', self.Media) + kwargs['form'] = form + return super().get_form(request, obj, **kwargs) def get_parent_instance(self, request=None, obj=None): diff --git a/cmsplugin_cascade/static/cascade/admin/clipboards/demo/demo_carousel-plugin.json b/cmsplugin_cascade/static/cascade/admin/clipboards/demo/demo_carousel-plugin.json new file mode 100644 index 000000000..b5350d77d --- /dev/null +++ b/cmsplugin_cascade/static/cascade/admin/clipboards/demo/demo_carousel-plugin.json @@ -0,0 +1,79 @@ +{ + "plugins": [ + [ + "BootstrapCarouselPlugin", + { + "glossary":{ + "hide_plugin":false, + "margins_xs":"", + "margins_sm":"", + "margins_md":"", + "margins_lg":"", + "interval":5, + "options":[ + "slide", + "pause", + "wrap" + ], + "container_max_heights":{ + "xs":"9rem", + "sm":"9rem", + "md":"9rem", + "lg":"9rem", + "xl":"9rem" + }, + "resize_options":[ + "upscale", + "crop", + "subject_location", + "high_resolution" + ] + }, + "pk":229 + }, + [ + [ + "BootstrapCarouselSlidePlugin", + { + "glossary":{ + "resize_options":[ + "upscale", + "crop", + "subject_location", + "high_resolution" + ], + "image":{ + "pk":4, + "model":"filer.Image" + }, + "media_queries":{ + "xs":{ + "width":572, + "media":"(max-width: 575.98px)" + }, + "sm":{ + "width":540, + "media":"(min-width: 576px) and (max-width: 767.98px)" + }, + "md":{ + "width":720, + "media":"(min-width: 768px) and (max-width: 991.98px)" + }, + "lg":{ + "width":960, + "media":"(min-width: 992px) and (max-width: 1199.98px)" + }, + "xl":{ + "width":1140, + "media":"(min-width: 1200px)" + } + } + }, + "pk":1526 + }, + [] + ] + ] + ] + ] +} diff --git a/cmsplugin_cascade/static/cascade/admin/import-export.png b/cmsplugin_cascade/static/cascade/admin/import-export.png new file mode 100644 index 000000000..f3ed60b38 Binary files /dev/null and b/cmsplugin_cascade/static/cascade/admin/import-export.png differ diff --git a/cmsplugin_cascade/static/cascade/css/admin/cascade_box.css b/cmsplugin_cascade/static/cascade/css/admin/cascade_box.css new file mode 100644 index 000000000..7550fa40c --- /dev/null +++ b/cmsplugin_cascade/static/cascade/css/admin/cascade_box.css @@ -0,0 +1,108 @@ +/* Generated by Glyphter (http://www.glyphter.com) on Wed Jan 08 2020*/ +@font-face { + font-family: 'cascade_box'; + src: url('../fonts/cascade_box.eot'); + src: url('../fonts/cascade_box.eot?#iefix') format('embedded-opentype'), + url('../fonts/cascade_box.woff') format('woff'), + url('../fonts/cascade_box.ttf') format('truetype'), + url('../fonts/cascade_box.svg#cascade_box') format('svg'); + font-weight: normal; + font-style: normal; +} +[class*='icon-']:before{ + display: inline-block; + font-family: 'cascade_box'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} +.icon-col:before{content:'\0041';} +.icon-col-1:before{content:'\0042';} +.icon-col-2:before{content:'\0043';} +.icon-col-3:before{content:'\0044';} +.icon-col-4:before{content:'\0045';} +.icon-col-5:before{content:'\0046';} +.icon-col-6:before{content:'\0047';} +.icon-col-7:before{content:'\0048';} +.icon-col-8:before{content:'\0049';} +.icon-col-9:before{content:'\004a';} +.icon-col-10:before{content:'\004b';} +.icon-col-11:before{content:'\004c';} +.icon-col-12:before{content:'\004d';} +.icon-col-auto:before{content:'\004e';} +.icon-col-inherit:before{content:'\004f';} +.icon-order-12:before{content:'\0050';} +.icon-offset-title:before{content:'\0051';} +.icon-column-title:before{content:'\0052';} +.icon-inherit:before{content:'\0053';} +.icon-container-title:before{content:'\0054';} +.icon-responsive-title:before{content:'\0055';} +.icon-xs:before{content:'\0056';} +.icon-sm:before{content:'\0057';} +.icon-md:before{content:'\0058';} +.icon-lg:before{content:'\0059';} +.icon-xl:before{content:'\005a';} +.icon-offtest-inherit:before{content:'\0061';} +.icon-offset-1:before{content:'\0062';} +.icon-offset-2:before{content:'\0063';} +.icon-offset-3:before{content:'\0064';} +.icon-offset-4:before{content:'\0065';} +.icon-offset-5:before{content:'\0066';} +.icon-offset-6:before{content:'\0067';} +.icon-offset-7:before{content:'\0068';} +.icon-offset-8:before{content:'\0069';} +.icon-offset-9:before{content:'\006a';} +.icon-offset-10:before{content:'\006b';} +.icon-offset-11:before{content:'\006c';} +.icon-offset-12:before{content:'\006d';} +.icon-order-1:before{content:'\006e';} +.icon-order-2:before{content:'\006f';} +.icon-order-3:before{content:'\0070';} +.icon-order-4:before{content:'\0071';} +.icon-order-5:before{content:'\0072';} +.icon-order-6:before{content:'\0073';} +.icon-order-7:before{content:'\0074';} +.icon-order-8:before{content:'\0075';} +.icon-order-9:before{content:'\0076';} +.icon-order-10:before{content:'\0077';} +.icon-order-11:before{content:'\0078';} +.icon-order-12:before{content:'\0079';} +.icon-hidden:before{content:'\007a';} +.icon-visible:before{content:'\0030';} +.icon-order-inherit:before{content:'\0031';} +.icon-visible-inherit:before{content:'\0032';} +.icon-reorder-title:before{content:'\0033';} +.icon-px-0:before{content:'\0034';} +.icon-px-1:before{content:'\0035';} +.icon-px-2:before{content:'\0036';} +.icon-px-3:before{content:'\0037';} +.icon-px-4:before{content:'\0038';} +.icon-px-5:before{content:'\0039';} +.icon-background_and_color-title:before{content:'\0021';} +.icon-p-0:before{content:'\0022';} +.icon-p-1:before{content:'\0023';} +.icon-p-2:before{content:'\0024';} +.icon-p-3:before{content:'\0025';} +.icon-p-4:before{content:'\0026';} +.icon-p-5:before{content:'\0027';} +.icon-paddings-title:before{content:'\0028';} +.icon-m-0:before{content:'\0029';} +.icon-m-1:before{content:'\002a';} +.icon-m-2:before{content:'\002b';} +.icon-m-3:before{content:'\002c';} +.icon-m-4:before{content:'\002d';} +.icon-m-5:before{content:'\002e';} +.icon-margins-title:before{content:'\002f';} +.icon-vertical_margins-title:before{content:'\005b';} +.icon-floats-title:before{content:'\005c';} +.icon-my-5:before{content:'\005d';} +.icon-float-none:before{content:'\005e';} +.icon-float-right:before{content:'\005f';} +.icon-float-left:before{content:'\0060';} +.icon-my-0:before{content:'\007b';} +.icon-my-1:before{content:'\007c';} +.icon-my-2:before{content:'\007d';} +.icon-my-3:before{content:'\007e';} +.icon-my-4:before{content:'\003f';} \ No newline at end of file diff --git a/cmsplugin_cascade/static/cascade/css/admin/clipboard.css b/cmsplugin_cascade/static/cascade/css/admin/clipboard.css index 522efe01c..abde920ab 100644 --- a/cmsplugin_cascade/static/cascade/css/admin/clipboard.css +++ b/cmsplugin_cascade/static/cascade/css/admin/clipboard.css @@ -11,3 +11,136 @@ background-color: #dff0d8; padding: 5px 10px; } + +div.cms .cms-structure .cms-submenu-item a[data-icon=import]:before, +div.cms .cms-structure .cms-submenu-item a[data-icon=export]:before { + position: absolute; + content: ''; + display: block !important; + width: 16px !important; + height: 16px !important; + background-image: url('../../admin/import-export.png') !important; + background-size: 16px 64px !important; + top: 15px !important; + overflow: hidden !important; + line-height: 16px !important; + min-height: 16px !important; +} +div.cms .cms-structure .cms-submenu-item a[data-icon=import]:before { + background-position: 0 -32px; +} +div.cms .cms-structure .cms-submenu-item a[data-icon=export]:hover:before, +div.cms .cms-structure .cms-submenu-item a[data-icon=export]:focus:before { + background-position: 0 -16px; +} +div.cms .cms-structure .cms-submenu-item a[data-icon=import]:hover:before, +div.cms .cms-structure .cms-submenu-item a[data-icon=import]:focus:before { + background-position: 0 -48px; +} +div.cms .cms-structure.cms-structure-condensed .cms-submenu-item a[data-icon=export]:before, +div.cms .cms-structure.cms-structure-condensed .cms-submenu-item a[data-icon=import]:before { + top: 12px !important; +} + + + +.container_clipboard { + display: flex; + justify-content: space-between; +} + +.cascade_clipboard_admin { + padding: 1rem; +} + +.button_clipboard { + background-color:#ffffff; + -webkit-border-radius:6px; + -moz-border-radius:6px; + border-radius:6px; + border:1px solid #dcdcdc; + cursor:pointer; + color:#666666; + font-family:Arial; + font-size:15px; + font-weight:bold; + padding:6px 24px; + text-decoration:none; +} +.button_clipboard:hover { + background-color:#f6f6f6; +} +.button_clipboard:active { + position:relative; + top:1px; +} + +.cms-admin-modal{ +background-color: white; +} +.field-placeholder, .field-language, #footer { + display:none; +} + +.field-clipboard { + border-bottom: none; +} + +.select-box__current { + display: inline-flex; + flex-wrap: wrap; + position:absolute; +} + +.cms-cascade-clipboard { + width:400px; + margin-right:1rem; + margin-bottom:1rem; +} + +.clipboard-base .clipboard-fullscreen:checked ~ .cms-cascade-clipboard { + width:100%; +} + +.clipboard-fullscreen { + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + -o-appearance: none; + appearance: none; + position: relative; + right: 3Opx; + left: 50%; + top: 26px; +} + +.clipboard-fullscreen:after{ + content:'fullscreen'; + cursor: pointer; +} + +.clipboard-fullscreen:checked:after { + content:'back'; + cursor: pointer; +} + +.clipboard-base .clipboard-fullscreen:checked { + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + -o-appearance: none; + appearance: none; +} + +.clipboard-base .clipboard-fullscreen:not(:checked), .clipboard-base .clipboard-fullscreen:not(:checked) ~ .cms-cascade-clipboard { + display:none; +} + +.clipboard-base .select-box__current { + display:inherit; +} + +.shortcuts-admin{ +display:none; +} + diff --git a/cmsplugin_cascade/static/cascade/css/admin/compact_forms/bootstrap4-colors.css b/cmsplugin_cascade/static/cascade/css/admin/compact_forms/bootstrap4-colors.css new file mode 100644 index 000000000..416077c8d --- /dev/null +++ b/cmsplugin_cascade/static/cascade/css/admin/compact_forms/bootstrap4-colors.css @@ -0,0 +1,234 @@ +.text-monospace { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; } + +.text-justify { + text-align: justify !important; } + +.text-wrap { + white-space: normal !important; } + +.text-nowrap { + white-space: nowrap !important; } + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + +.text-left { + text-align: left !important; } + +.text-right { + text-align: right !important; } + +.text-center { + text-align: center !important; } + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; } + .text-sm-right { + text-align: right !important; } + .text-sm-center { + text-align: center !important; } } + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; } + .text-md-right { + text-align: right !important; } + .text-md-center { + text-align: center !important; } } + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; } + .text-lg-right { + text-align: right !important; } + .text-lg-center { + text-align: center !important; } } + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; } + .text-xl-right { + text-align: right !important; } + .text-xl-center { + text-align: center !important; } } + +.text-lowercase { + text-transform: lowercase !important; } + +.text-uppercase { + text-transform: uppercase !important; } + +.text-capitalize { + text-transform: capitalize !important; } + +.font-weight-light { + font-weight: 300 !important; } + +.font-weight-lighter { + font-weight: lighter !important; } + +.font-weight-normal { + font-weight: 400 !important; } + +.font-weight-bold { + font-weight: 700 !important; } + +.font-weight-bolder { + font-weight: bolder !important; } + +.font-italic { + font-style: italic !important; } + +.text-white { + color: #fff !important; } + +.text-primary { + color: #007bff !important; } + +a.text-primary:hover, a.text-primary:focus { + color: #0056b3 !important; } + +.text-secondary { + color: #6c757d !important; } + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; } + +.text-success { + color: #28a745 !important; } + +a.text-success:hover, a.text-success:focus { + color: #19692c !important; } + +.text-info { + color: #17a2b8 !important; } + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; } + +.text-warning { + color: #ffc107 !important; } + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; } + +.text-danger { + color: #dc3545 !important; } + +a.text-danger:hover, a.text-danger:focus { + color: #a71d2a !important; } + +.text-light { + color: #f8f9fa !important; } + +a.text-light:hover, a.text-light:focus { + color: #cbd3da !important; } + +.text-dark { + color: #343a40 !important; } + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; } + +.text-body { + color: #212529 !important; } + +.text-muted { + color: #6c757d !important; } + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; } + +.text-white-50 { + color: rgba(255, 255, 255, 0.5) !important; } + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; } + +.text-decoration-none { + text-decoration: none !important; } + +.text-break { + word-break: break-word !important; + overflow-wrap: break-word !important; } + +.text-reset { + color: inherit !important; } + +.bg-primary { + background-color: #007bff !important; } + +a.bg-primary:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; } + +.bg-secondary { + background-color: #6c757d !important; } + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; } + +.bg-success { + background-color: #28a745 !important; } + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; } + +.bg-info { + background-color: #17a2b8 !important; } + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; } + +.bg-warning { + background-color: #ffc107 !important; } + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; } + +.bg-danger { + background-color: #dc3545 !important; } + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; } + +.bg-light { + background-color: #f8f9fa !important; } + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; } + +.bg-dark { + background-color: #343a40 !important; } + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + background-color: #1d2124 !important; } + +.bg-white { + background-color: #fff !important; } + +.bg-transparent { + background-color: transparent !important; } diff --git a/cmsplugin_cascade/static/cascade/css/admin/compact_forms/bootstrap4-colors.scss b/cmsplugin_cascade/static/cascade/css/admin/compact_forms/bootstrap4-colors.scss new file mode 100644 index 000000000..17f7df0e1 --- /dev/null +++ b/cmsplugin_cascade/static/cascade/css/admin/compact_forms/bootstrap4-colors.scss @@ -0,0 +1,11 @@ +@import "bootstrap/scss/_functions"; +@import "bootstrap/scss/_variables"; +@import "bootstrap/scss/mixins/deprecate"; +@import "bootstrap/scss/mixins/background-variant"; +@import "bootstrap/scss/mixins/breakpoints"; +@import "bootstrap/scss/mixins/hover"; +@import "bootstrap/scss/mixins/text-hide"; +@import "bootstrap/scss/mixins/text-truncate"; +@import "bootstrap/scss/mixins/text-emphasis"; +@import "bootstrap/scss/utilities/text"; +@import "bootstrap/scss/utilities/background"; diff --git a/cmsplugin_cascade/static/cascade/css/admin/compact_forms/main_compact_form.css b/cmsplugin_cascade/static/cascade/css/admin/compact_forms/main_compact_form.css new file mode 100644 index 000000000..b09afd1d5 --- /dev/null +++ b/cmsplugin_cascade/static/cascade/css/admin/compact_forms/main_compact_form.css @@ -0,0 +1,367 @@ + +.form-row[class*="column-width"], +.form-row[class*="offset"], +.form-row[class*="column-ordering"], +.form-row[class*="responsive"], +.form-row[class*="float"]{ + +} + + + +.fieldBox[class*="column-width"] div.help, +.fieldBox[class*="column-ordering"] div.help, +.fieldBox[class*="responsive"] div.help, +.fieldBox[class*="offset"] div.help { + display:none; +} + + +.nested .fieldBox > label:first-child{ + display:none; +} + +.cascade_box { +display: flex; +justify-content: center; +margin-bottom: 0px; +} + +.cascade_box .form-row .fieldBox{ +margin-right:0px; +margin-top:0px; +padding:0px; +margin:0px; +} + + +.cascade_box .form-row .fieldBox .container-thumbnail{ +display:inline-grid; +} + +.cascade_box .form-row .field-breakpoints{ + +width: 100%; +display: grid; +} + +.cascade_box .form-row .container-thumbnail label { + width: 82px !important; + text-align: center; +} + +.fieldBox.field-fluid{ +padding: 2rem !important; +display: inherit; +} + +.cascade_box .form-row{ + width: 463px; +} + +.nested .form-row{ + padding:0px; +} + +.cascade_box .field-background_and_color div > label:first-child{ +opacity:0; +position:absolute; +} + +.cascade_box .fieldBox.form-row{ +padding: 0; +} + +.cascade_box h2{ + width: 65px; +border: 1px solid; +} + + +.fieldBox[class*="data-sal"], +.fieldBox[class*="inline_styles:margin"], +.fieldBox[class*="inline_styles:padding"]{ + display: inline-grid !important; +} + + +input[name="cascade-input"] + label{ + width:100% ; + padding: 0px; +} + +/* overide django form.css*/ +input[name="cascade-input"] + label::after { + content: none !important; +} + +input[name="cascade-input"] { + opacity:0; +} + + + + + +.form-row .field-background_and_color{ + width: 548px; +} + +.input__container{ + opacity:0; + position:absolute; +} + + +.select-box__option i[class*="icon-"]{ + font-size:60px; +} + +svg { +z-index:42; + +} + + + +.select-box__option div[class*="icon-"]{ +font-size:60px; + +} + +.min_overlay{ +position:absolute; +opacity:0 !important; +display: inline-block; +width: 60px; +height: 60px; +} + +.min_overlay + label{ +width:100%; +} + + +form .form-row.field-cms_page { + height: 100% !important; + float: left; +} + +fieldset .fieldBox { + float: left; + margin-right: 20px; + display: inline-flex; +} + + +.select-box { + position: absolute; + display: contents; + font-family: "Open Sans", "Helvetica Neue", "Segoe UI", "Calibri", "Arial", sans-serif; + font-size: 18px; + color: #60666d; +} + +.select-box help { + display: none; +} + + +.select-box__current { + position: relative; + box-shadow: 0 15px 30px -10px rgba(0, 0, 0, 0.1); + outline: none; + width: 90px +} + +.select-box__current:focus > .select-box__icon { + -webkit-transform: translateY(-50%) rotate(180deg); + transform: translateY(-50%) rotate(180deg); +} + +.select-box__option:hover, .select-box__option:focus { + color: #546c84; + background-color: #fbfbfb; +} + .select-box__input{ + display: none; +} + +.select-box__option a:active + { + color: #546c84; + background-color: #fbfbfb; + // pointer-events:none; +} + +.select-box__input:checked + .select-box__input-text { + display: inline-grid !important; + +} + +.select-box__option { + display: block; + background-color: #fff; +} + +.select-box__input { + display: none; +} + +.select-box__input-text { + display: none; + width: 100%; + cursor:pointer; +} + + +.select-box__input-text::after { + display: block !important; + width: 100%; + background-color: red; +} + +.select-box__list li label { + padding: 0 !important; +} + +.select-box__list{ + overflow : hidden; + position: absolute; + width: 96%; + padding:10px; + + list-style: none; + display: inline-flex; + flex-wrap: wrap; + left:0px; + font-size:400; + justify-content: center; + margin-left: 0 !important; + padding-left: 0!important; + box-shadow: 0 15px 30px -10px rgba(0, 0, 0, 0.1); +} + + +.select-box__option{ + padding: 15px; + background-color: #fff; +} + + +svg { + width: 2px; + height:1px; + position: absolute; + left: 0px; +} + +.form-row.field-button_type { + max-width: none !important; +} + + +.form-row[class*="background_and_color"] svg foreignObject a { +pointer-events:all; +} + +.form-row[class*="background_and_color"] div labeel { +opacity: 0; +position: absolute; +left: -1000px; +} + + +input[name="cascade-input"]:focus-within:checked + label .select-box svg foreignObject { +width:100%; +height:100%; +} + +input[name="cascade-input"]:focus-within:checked + label .select-box svg { +width:100% ; +height:100%; +} + + + +input[name="cascade-input"]:focus-within:checked + label .select-box svg foreignObject div .select-box__list { +clip-path : unset; +background-color: white !important; +pointer-events:visible; +height: auto; +opacity: 1; + -webkit-animation-name: none; +animation-name: none; +z-index:9900; +left:0px; +} + + +.description { +background:#79aec8; +width:70px; +color:white; +} + + +.icon_desc { +font-size: xx-large; +} + + +.select-box__icon { + position: absolute; + top: 50%; + right: 15px; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + width: 20px; + opacity: 0.3; + transition: 0.2s ease; +} + + +.select-box__value { + display: block; + text-align: center; + width:90px; +} + +.buttons ul { +background: white; +} + +.buttons ul a { +padding:10px !important; +pointer-events: none; + +} + +.buttons ul label{ +padding:0,4rem; +} + +.buttons ul div{ +cursor:pointer; +} + +.form-row .fieldBox > label[for*=""] { + display: none; + font-family:'border box'; + font-size: xx-large; +} + + +.field-button_type .select-box__input-text { + padding: 2px !important; + width: auto; + background-color: none; +} + +.select-box__list label { +font-size: none !important; +} + +.detail-select-icon { + color:black; +} + diff --git a/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.eot b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.eot new file mode 100644 index 000000000..df6631620 Binary files /dev/null and b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.eot differ diff --git a/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.svg b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.svg new file mode 100644 index 000000000..19f8d5cf8 --- /dev/null +++ b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.svg @@ -0,0 +1 @@ +Generated by Glyphter \ No newline at end of file diff --git a/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.ttf b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.ttf new file mode 100644 index 000000000..c9ca89831 Binary files /dev/null and b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.ttf differ diff --git a/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.woff b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.woff new file mode 100644 index 000000000..b3d1ec96f Binary files /dev/null and b/cmsplugin_cascade/static/cascade/css/fonts/cascade_box.woff differ diff --git a/cmsplugin_cascade/static/cascade/js/admin/clipboard_gallery.js b/cmsplugin_cascade/static/cascade/js/admin/clipboard_gallery.js new file mode 100644 index 000000000..62684d180 --- /dev/null +++ b/cmsplugin_cascade/static/cascade/js/admin/clipboard_gallery.js @@ -0,0 +1,45 @@ +const div_select_box = document.querySelector(".select-box"); +const svg_stride = document.querySelectorAll(".cms-cascade-svg-viewer"); +const input_fullscreen = document.querySelectorAll(".clipboard-fullscreen"); +const xs = document.getElementById("js-cascade-btn-xs"); +const sm = document.getElementById("js-cascade-btn-sm"); +const md = document.getElementById("js-cascade-btn-md"); +const lg = document.getElementById("js-cascade-btn-lg"); +const xl = document.getElementById("js-cascade-btn-xl"); +const max = document.getElementById("js-cascade-btn-max"); + +input_fullscreen.forEach(element => { + element.addEventListener('click', function(event) { + if ( div_select_box.classList.contains('clipboard-base')){ + div_select_box.classList.remove('clipboard-base'); + } + else { + div_select_box.classList.add('clipboard-base'); + } + }); +}); + +function parser_clipboards(elements, viewbox){ + elements.forEach(element => { + element.setAttribute("viewBox", viewbox); + }); +} + +xs.addEventListener('click', function(event) { + parser_clipboards(svg_stride ,'0 0 475 500'); + }); +sm.addEventListener('click', function(event) { + parser_clipboards(svg_stride ,'0 0 767 800'); + }); +md.addEventListener('click', function(event) { + parser_clipboards(svg_stride ,'0 0 991 900'); + }); +lg.addEventListener('click', function(event) { + parser_clipboards(svg_stride ,'0 0 1199 1400'); + }); +xl.addEventListener('click', function(event) { + parser_clipboards(svg_stride ,'0 0 2000 4000'); + }); +max.addEventListener('click', function(event) { + parser_clipboards(svg_stride ,'0 0 5000 10000'); + }); diff --git a/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js b/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js index 6b0f11e4a..e5b6e50a0 100644 --- a/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js +++ b/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js @@ -41,6 +41,7 @@ django.jQuery(function($) { this.linkTypes['download'] = new this.LinkType('.form-row.field-download_file'); this.linkTypes['exturl'] = new this.LinkType('.form-row.field-ext_url', true); this.linkTypes['email'] = new this.LinkType('.form-row.field-mail_to'); + this.linkTypes['phonenumber'] = new this.LinkType('.form-row.field-phone_number'); }, toggleLinkTypes: function(linkTypeName) { $.each(this.linkTypes, function(name, linkType) { diff --git a/cmsplugin_cascade/strides.py b/cmsplugin_cascade/strides.py index e0c11991b..96ca1454f 100644 --- a/cmsplugin_cascade/strides.py +++ b/cmsplugin_cascade/strides.py @@ -11,7 +11,9 @@ from cmsplugin_cascade import app_settings from cmsplugin_cascade.mixins import CascadePluginMixin - +from collections import defaultdict +from sekizai.data import UniqueSequence +from random import randint __all__ = ['register_stride', 'StrideContentRenderer'] @@ -30,6 +32,8 @@ class StrideElementBase(object): """ def __init__(self, plugin, data, children_data, parent=None): self.plugin = plugin + #pass args radom id + data['pk'] = randint(0, 20000) self.id = data.get('pk') self.glossary = data.get('glossary', {}) self.sortinline_elements = self.inline_elements = EmulateQuerySet(data.get('inlines', [])) @@ -151,6 +155,10 @@ def _get_render_template(self, context, instance, placeholder): if not template: raise TemplateDoesNotExist("plugin {} has no render_template".format(self.__class__)) + + if hasattr(instance.plugin, 'render_template_fallback'): + template = instance.plugin.render_template_fallback + return template def in_edit_mode(self, request, placeholder): @@ -183,8 +191,9 @@ def render(self, context, instance, placeholder): class StrideContentRenderer(object): def __init__(self, request): - self.request = request - self.language = get_language_from_request(request) + if request: + self.request = request + self.language = get_language_from_request(request) self._cached_templates = {} def render_cascade(self, context, tree_data): @@ -205,26 +214,32 @@ def render_plugin(self, instance, context, placeholder=None, editable=False): from sekizai.helpers import get_varname as get_sekizai_context_key sekizai_context_key = get_sekizai_context_key() + context[sekizai_context_key] = defaultdict(UniqueSequence) + if app_settings.CMSPLUGIN_CASCADE['cache_strides'] and getattr(instance.plugin, 'cache', not editable): + cache = caches['default'] key = 'cascade_element-{}'.format(instance.pk) content = cache.get(key) if content: - context[sekizai_context_key]['css'].extend(cache.get(key + ':css_list', [])) - context[sekizai_context_key]['js'].extend(cache.get(key + ':js_list', [])) + if sekizai_context_key in context: + context[sekizai_context_key]['css'].extend(cache.get(key + ':css_list', [])) + context[sekizai_context_key]['js'].extend(cache.get(key + ':js_list', [])) return content else: context['cms_cachable_plugins'].value = False context = instance.plugin.render(context, instance, placeholder) context = flatten_context(context) - template = instance.plugin._get_render_template(context, instance, placeholder) + template = instance.plugin._get_render_template( context, instance, placeholder) template = self.get_cached_template(template) + content = template.render(context) if context['cms_cachable_plugins'].value: cache.set(key, content) - cache.set(key + ':css_list', context[sekizai_context_key]['css'].data) - cache.set(key + ':js_list', context[sekizai_context_key]['js'].data) + if sekizai_context_key in context: + cache.set(key + ':css_list', context[sekizai_context_key]['css'].data) + cache.set(key + ':js_list', context[sekizai_context_key]['js'].data) return content def user_is_on_edit_mode(self): diff --git a/cmsplugin_cascade/templates/cascade/admin/change_form.html b/cmsplugin_cascade/templates/cascade/admin/change_form.html index eed0c9b39..f8fb77fab 100644 --- a/cmsplugin_cascade/templates/cascade/admin/change_form.html +++ b/cmsplugin_cascade/templates/cascade/admin/change_form.html @@ -1,6 +1,7 @@ {% extends "admin/cms/usersettings/change_form.html" %} {% load admin_static admin_urls i18n %} + {% block field_sets %} {{ plugin_intro }} {% if empty_form %} diff --git a/cmsplugin_cascade/templates/cascade/admin/clipboard_close_frame.html b/cmsplugin_cascade/templates/cascade/admin/clipboard_close_frame.html new file mode 100644 index 000000000..cf8688273 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/clipboard_close_frame.html @@ -0,0 +1,5 @@ +
+
+
+
+
diff --git a/cmsplugin_cascade/templates/cascade/admin/clipboard_paste_plugins.html b/cmsplugin_cascade/templates/cascade/admin/clipboard_paste_plugins.html new file mode 100644 index 000000000..187f7547d --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/clipboard_paste_plugins.html @@ -0,0 +1,8 @@ + +
+
+
+
+
diff --git a/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/container_breakpoints.html b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/container_breakpoints.html new file mode 100644 index 000000000..b7db6cec3 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/container_breakpoints.html @@ -0,0 +1,10 @@ +{% load static %}{% spaceless %} +
+{% for group, options, index in widget.optgroups %}{% with widget=options.0 %} +
+
+ +
{% include widget.template_name %}
+
+
{% endwith %}{% endfor %} +
{% endspaceless %} diff --git a/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_button_types.html b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_button_types.html new file mode 100644 index 000000000..7f59e4bd2 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_button_types.html @@ -0,0 +1,44 @@ +{% load static %} +{% spaceless %} + +{% with id=widget.attrs.id %} + + +{% endwith %} +{% endspaceless %} + diff --git a/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_colors.html b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_colors.html new file mode 100644 index 000000000..421adb81b --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_colors.html @@ -0,0 +1,44 @@ +{% load static %} +{% spaceless %} + +{% with id=widget.attrs.id %} + + +{% endwith %} +{% endspaceless %} diff --git a/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_columns.html b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_columns.html new file mode 100644 index 000000000..d4ec1ecfe --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/compact_forms/widgets/select_icon_columns.html @@ -0,0 +1,79 @@ +{% load static %} +{% spaceless %} +{% with id=widget.attrs.id %} + + +{% endwith %} +{% endspaceless %} + + + diff --git a/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard.html b/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard.html new file mode 100644 index 000000000..ad77e2fe0 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard.html @@ -0,0 +1,37 @@ +{% load static cascade_tags sekizai_tags sass_tags %} +{% spaceless %} + +{% block extrastyle %} + + +{% endblock %} + +{% include "cascade/admin/widgets/clipboard_menu_group.html" %} + +{% with id=widget.attrs.id %} + + + +{% block after_field_sets %} + + +{% endblock %} + +{% block footer%} +{% endblock %} +{% endwith %} +{% endspaceless %} diff --git a/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard_menu_group.html b/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard_menu_group.html new file mode 100644 index 000000000..3fe950d69 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard_menu_group.html @@ -0,0 +1,49 @@ +{% load static %} + +
+ {% for group in groups_exclude_home %} + {{ group }} + {% endfor %} + {% if widget.attrs.count_target %} + Ungroup + {% endif %} +
diff --git a/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard_stride.html b/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard_stride.html new file mode 100644 index 000000000..d96f40536 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/admin/widgets/clipboard_stride.html @@ -0,0 +1,15 @@ +{% load i18n cms_admin cascade_tags %} +
+ + +{{ option.label }} +
+ + +
+ {% render_cascade data option.label %} +
+
+
+
+
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html b/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html index ae3d899c4..30c226d35 100644 --- a/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html @@ -1,13 +1,13 @@ {% load l10n cascade_tags %} - {% localize off %}{% spaceless %}{% with inline_styles=instance.inline_styles plugin_id=instance.id %}
{% for card in instance.child_plugin_instances %} + {{ card.glossary.accordion_nested.heading }}
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/image.html b/cmsplugin_cascade/templates/cascade/bootstrap4/image.html index ab4e8c093..fb9492dfd 100644 --- a/cmsplugin_cascade/templates/cascade/bootstrap4/image.html +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/image.html @@ -1,15 +1,15 @@ -{% load l10n static cascade_tags thumbnail %} +{% load l10n static cascade_tags thumbnail %} {% localize off %}{% spaceless %}{% with css_classes=instance.css_classes inline_styles=instance.inline_styles %} - {% else %} - src="{% static 'cascade/fallback.svg' %}" +{% include "cascade/generic/fallback_image.html" %} {% endif %} -/> {% endwith %}{% endspaceless %}{% endlocalize %} diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/jumbotron.html b/cmsplugin_cascade/templates/cascade/bootstrap4/jumbotron.html index fdffc3a0d..af6ca79fe 100644 --- a/cmsplugin_cascade/templates/cascade/bootstrap4/jumbotron.html +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/jumbotron.html @@ -28,6 +28,16 @@ } } {% endfor %} +{% elif 'pk' in instance.glossary.image_file %} + #cascadeelement_id-{{ instance.pk }} { + {% fallback instance %} + } +{% endif %} +{% if request.toolbar and request.toolbar.edit_mode_active %} +{% if 'fixed-top' in instance.glossary.values and instance.get_first_child.plugin_type == 'BootstrapNavbarPlugin' %} + body { padding-top:2.8rem } + .jumbotron-fluid { margin-top: 2.8rem !important ;} +{% endif %} {% endif %} {% endlocalize %}{% endaddtoblock %} @@ -37,5 +47,5 @@ {% for plugin in instance.child_plugin_instances %} {% render_plugin plugin %} {% endfor %} -
+
{% endspaceless %}{% endlocalize %} diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar.html new file mode 100644 index 000000000..fb75c1a3c --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar.html @@ -0,0 +1,25 @@ +{% load l10n cascade_tags thumbnail sekizai_tags %} +{% block extra-styles %}{% endblock %} +{% block extra-scripts %}{% endblock %} + +{# The Navbar plug-in compensates for the height of the CMS toolbar #} +{% if request.toolbar and request.toolbar.edit_mode_active %} + {% if instance.plugin_type == 'BootstrapNavbarPlugin' and 'fixed-top' in instance.css_classes %} + +{% endif %} +{% endif %} + +{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %} + diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand.html new file mode 100644 index 000000000..43aa0aa49 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand.html @@ -0,0 +1,13 @@ +{% extends "cascade/link/link-base.html" %} +{% load l10n cascade_tags sekizai_tags %} +{% block link_link %}{% with instance_link=instance|default:"#" %} + {% block navbar-brand %} + {% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %} + {% for child in instance.child_plugin_instances %} + {{ block.super }} + {% render_plugin child %} + {% endfor %} +{% endwith %} + {% endblock %} +{% endwith %} +{% endblock %} diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand_image.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand_image.html new file mode 100644 index 000000000..3a818c2ed --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand_image.html @@ -0,0 +1,14 @@ +{% load l10n thumbnail %} +{% localize off %}{% spaceless %} +{% with css_classes=instance.css_classes inline_styles=instance.inline_styles %} +{% if instance.image %} {% if not instance.image %}{% endif %} + {% endwith %} +{% endspaceless %}{% endlocalize %} + diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_collapse.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_collapse.html new file mode 100644 index 000000000..e24b26e51 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_collapse.html @@ -0,0 +1,7 @@ +{% load l10n cascade_tags sekizai_tags %} +{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %} + +{% endwith %} diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_links.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_links.html new file mode 100644 index 000000000..dd5416f12 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_links.html @@ -0,0 +1,4 @@ +{% load bootstrap_tags cms_tags %} + diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_list.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_list.html new file mode 100644 index 000000000..18b3f63b2 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_list.html @@ -0,0 +1,18 @@ +{% load l10n cascade_tags sekizai_tags %} +{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles child_css_classes=instance.glossary.child_css_classes %} +{% block navbar-nav-list %}
    +{% for child in instance.child_plugin_instances %} +
  • +{% render_plugin child %} +
  • +{% endfor %} +
+{% endblock %} +{% endwith %} + + diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_items_li_menu_main_links.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_items_li_menu_main_links.html new file mode 100644 index 000000000..7d84b4e13 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_items_li_menu_main_links.html @@ -0,0 +1,4 @@ +{% load bootstrap_tags l10n cascade_tags sekizai_tags %} +{% block navbar-nav %} +{% main_menu "bootstrap4/menu/navbar.html" %} +{% endblock %} diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_toogler.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_toogler.html new file mode 100644 index 000000000..328cae2d2 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_toogler.html @@ -0,0 +1,9 @@ +{% load cms_tags %} +{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles child_css_classes=instance.glossary.child_css_classes %} + +{% endwith %} + + + diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/picture.html b/cmsplugin_cascade/templates/cascade/bootstrap4/picture.html index 0db1a5cb7..a902e9e85 100644 --- a/cmsplugin_cascade/templates/cascade/bootstrap4/picture.html +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/picture.html @@ -2,6 +2,7 @@ {% localize off %}{% spaceless %} {% if instance.image|is_valid_image %} + {% for elem in elements %} {% thumbnail instance.image elem.size zoom=elem.zoom crop=elem.crop upscale=elem.upscale subject_location=elem.subject_location as thumb %} {% if elem.size2 %} @@ -14,7 +15,8 @@ {% endif %} {% endfor %} {% else %} - +
{# fallback instance #}
+ {% include "cascade/generic/fallback_picture.html" %} {% endif %} {% endspaceless %}{% endlocalize %} diff --git a/cmsplugin_cascade/templates/cascade/generic/fallback_image.html b/cmsplugin_cascade/templates/cascade/generic/fallback_image.html new file mode 100644 index 000000000..f07d033c8 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/generic/fallback_image.html @@ -0,0 +1,29 @@ +{% load l10n static thumbnail cascade_tags %} + +{% if instance.glossary.image_properties.width != src.size.0 or instance.glossary.image_properties.height != src.size.1 %} +{% fallback_config 'image' as fallback_image %} +{% if not 'img-fluid' in instance.glossary.image_shapes %} +
+ +
+{% else %} +
+ +
+{% endif %} +{% else %} +
+ +
+{% endif %} diff --git a/cmsplugin_cascade/templates/cascade/generic/fallback_jumbotron.html b/cmsplugin_cascade/templates/cascade/generic/fallback_jumbotron.html new file mode 100644 index 000000000..e19f04fd4 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/generic/fallback_jumbotron.html @@ -0,0 +1,3 @@ +{% load l10n cascade_tags thumbnail sekizai_tags %} +{% render_block 'css' %}{% block css %}{% endblock %} +{% include 'cascade/bootstrap4/jumbotron.html' %} diff --git a/cmsplugin_cascade/templates/cascade/generic/fallback_picture.html b/cmsplugin_cascade/templates/cascade/generic/fallback_picture.html new file mode 100644 index 000000000..44fcf9bd0 --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/generic/fallback_picture.html @@ -0,0 +1,32 @@ +{% load l10n thumbnail cascade_tags %} +{% fallback_config 'picture' as fallback_picture %} + +
+ +
diff --git a/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html b/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html index 5c1f88396..b61615b78 100644 --- a/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html +++ b/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html @@ -1,4 +1,4 @@ -{% load sekizai_tags %} +{% load sekizai_tags cascade_tags %} {% if stylesheet_url %}{% addtoblock "css" %}{% endaddtoblock %}{% endif %} {% spaceless %} {% with instance_link=instance.link tag_type=instance.tag_type css_classes=instance.css_classes inline_styles=instance.inline_styles %} @@ -7,7 +7,7 @@ {% elif css_classes or inline_styles %} {% endif %} - + {{ instance.glossary.content }} {% if instance_link %}{% elif css_classes or inline_styles %}{% endif %} {% endwith %} -{% endspaceless %} \ No newline at end of file +{% endspaceless %} diff --git a/cmsplugin_cascade/templatetags/cascade_tags.py b/cmsplugin_cascade/templatetags/cascade_tags.py index b42839f80..490994a66 100644 --- a/cmsplugin_cascade/templatetags/cascade_tags.py +++ b/cmsplugin_cascade/templatetags/cascade_tags.py @@ -11,6 +11,7 @@ from classytags.arguments import Argument from classytags.core import Options, Tag from cmsplugin_cascade.strides import StrideContentRenderer +from django.templatetags.static import static register = template.Library() @@ -21,30 +22,47 @@ class StrideRenderer(Tag): {% render_cascade "cascade-data.json" %} Keyword arguments: - datafile -- Filename containing the cascade tree. Must be file locatable by Django's + data_clipboard -- Filename containing the cascade tree. Must be file locatable by Django's static file finders. + identifier --- Identifier persitent clipboard db. """ name = 'render_cascade' options = Options( - Argument('datafile'), + Argument('data_clipboard'), + Argument('identifier', required=False), ) - def render_tag(self, context, datafile): + def render_tag(self, context, data_clipboard, identifier=None): from sekizai.helpers import get_varname as get_sekizai_context_key from cmsplugin_cascade.strides import StrideContentRenderer - + tr=False + if isinstance(data_clipboard, dict): + # qs_clipboards + identifier = identifier + datafile = False + elif isinstance(data_clipboard, str): + # relative path + datafile = data_clipboard + identifier = datafile + + tree_data_key = 'cascade-strides:' + identifier cache = caches['default'] - tree_data_key = 'cascade-strides:' + datafile tree_data = cache.get(tree_data_key) if cache else None - if tree_data is None: - jsonfile = finders.find(datafile) - if not jsonfile: - raise IOError("Unable to find file: {}".format(datafile)) - - with io.open(jsonfile) as fp: - tree_data = json.load(fp) - content_renderer = StrideContentRenderer(context['request']) + if tree_data is None: + if datafile : + jsonfile = finders.find(datafile) + if not jsonfile: + raise IOError("Unable to find file: {}".format(datafile)) + with io.open(jsonfile) as fp: + tree_data = json.load(fp) + else: + tree_data = data_clipboard + if 'request' in context: + data_req = context['request'] + else: + data_req = None + content_renderer = StrideContentRenderer(data_req) with context.push(cms_content_renderer=content_renderer): content = content_renderer.render_cascade(context, tree_data) @@ -71,12 +89,9 @@ class RenderPlugin(Tag): Argument('plugin') ) - def render_tag(self, context, plugin): + def render_tag(self, context, plugin, request=None): if not plugin: return '' - - request = context['request'] - toolbar = get_toolbar_from_request(request) if 'cms_content_renderer' in context and isinstance(context['cms_content_renderer'], StrideContentRenderer): content_renderer = context['cms_content_renderer'] elif 'cms_renderer' in context: @@ -84,17 +99,102 @@ def render_tag(self, context, plugin): elif 'cms_content_renderer' in context: content_renderer = context['cms_content_renderer'] else: + request = context['request'] + toolbar = get_toolbar_from_request(request) content_renderer = toolbar.content_renderer + try: + toolbar_edit_mode = toolbar.edit_mode_active + except UnboundLocalError: + toolbar_edit_mode = True content = content_renderer.render_plugin( instance=plugin, context=context, - editable=toolbar.edit_mode_active, + editable=toolbar_edit_mode ) return content register.tag('render_plugin', RenderPlugin) +class FallBack(Tag): + name = 'fallback' + + options = Options( + Argument('plugin',required=False) + ) + + def render_tag(self, context, plugin): + for context_ in context: + if 'instance'in context_ : + glossary = context_['instance'].glossary + instance = context_['instance'] + fallback_plugin_type = plugin.plugin_class.__name__ + css_classes = glossary.get('css_classes','') + width = 0; height = 0; exif_orientation = 0; x = 0; y = 0; + inline_styles = glossary.get('inline_styles','') + html_tag_attributes = glossary.get('html_tag_attributes','') + + if 'image' in glossary: + image_fallback = 'image' + img = settings.CMSPLUGIN_CASCADE["fallback"]["image"]['svg'] + color = settings.CMSPLUGIN_CASCADE["fallback"]["image"]['color'] + static_fallback_svg = static(img) + elif fallback_plugin_type == 'BootstrapJumbotronPlugin': + image_fallback=None + img = settings.CMSPLUGIN_CASCADE["fallback"]["jumbotron"]['svg'] + color = settings.CMSPLUGIN_CASCADE["fallback"]["jumbotron"]['color'] + static_fallback_svg = static(img) + elif 'image_properties' in glossary: + image_fallback='image_properties' + img = settings.CMSPLUGIN_CASCADE["fallback"]["picture"]['svg'] + color = settings.CMSPLUGIN_CASCADE["fallback"]["picture"]['color'] + static_fallback_svg = static(img) + else: + image_fallback=None + img = settings.CMSPLUGIN_CASCADE["fallback"]["picture"]['svg'] + color = settings.CMSPLUGIN_CASCADE["fallback"]["picture"]['color'] + static_fallback_svg = static(img) + if image_fallback : + width = glossary[image_fallback].get('width',0) + height = glossary[image_fallback].get('height',0) + exif_orientation = glossary[image_fallback].get('exif_orientation',0) + + x = 50 + y = 50 + + if fallback_plugin_type == 'BootstrapJumbotronPlugin': + style=''' + background: url({static_fallback_svg}); + background-size: auto; + background-position-y: 20%; + background-size: 50%; + background-repeat: no-repeat; + background-position-x: 50%; + background-attachment: fixed; + background-color: {color}; + border: white solid; + '''.format( color=color, static_fallback_svg=static_fallback_svg) + + return style + else: + svg=' \ + \ + '.format( + width=width, + height=height, + color=color, + css_classes=css_classes, + inline_styles=inline_styles, + html_tag_attributes=html_tag_attributes, + static_fallback_svg=static_fallback_svg, + x=x, + y=y) + return svg + +register.tag('fallback', FallBack) + @register.filter def is_valid_image(image): try: @@ -110,3 +210,13 @@ def sphinx_docs_include(path): raise TemplateDoesNotExist("'{path}' does not exist".format(path=path)) with io.open(filename) as fh: return mark_safe(fh.read()) + + +@register.simple_tag +def cascadeclipboard_data_by_identifier(queryset, identifier ): + qs_identifier=queryset.filter(identifier=identifier) + return qs_identifier[0].data + +@register.simple_tag +def fallback_config(type_fallback): + return settings.CMSPLUGIN_CASCADE["fallback"][type_fallback] diff --git a/cmsplugin_cascade/utils.py b/cmsplugin_cascade/utils.py index 3142da131..14a1480a4 100644 --- a/cmsplugin_cascade/utils.py +++ b/cmsplugin_cascade/utils.py @@ -2,7 +2,7 @@ from django.forms import MediaDefiningClass from django.utils.translation import ugettext_lazy as _ from entangled.forms import EntangledModelFormMixin - +from cmsplugin_cascade import app_settings def remove_duplicates(lst): """ @@ -48,11 +48,19 @@ def compute_aspect_ratio(image): def compute_aspect_ratio_with_glossary(glossary): - if glossary['image']['exif_orientation'] > 4: - # image is rotated by 90 degrees, while keeping width and height - return float(glossary['image']['width']) / float(glossary['image']['height']) - else: - return float(glossary['image']['height']) / float(glossary['image']['width']) + if 'image' in glossary : + if glossary['image']['exif_orientation'] > 4: + # image is rotated by 90 degrees, while keeping width and height + return float(glossary['image']['width']) / float(glossary['image']['height']) + else: + return float(glossary['image']['height']) / float(glossary['image']['width']) + # fallogic logic + elif 'image_properties' in glossary: + if glossary['image_properties']['exif_orientation'] > 4: + # image is rotated by 90 degrees, while keeping width and height + return float(glossary['image_properties']['width']) / float(glossary['image_properties']['height']) + else: + return float(glossary['image_properties']['height']) / float(glossary['image_properties']['width']) def get_image_size(width, image_height, aspect_ratio): @@ -86,23 +94,45 @@ class CascadeUtilitiesMixin(metaclass=MediaDefiningClass): """ If a Cascade plugin is listed in ``settings.CMSPLUGIN_CASCADE['plugins_with_extra_mixins']``, then this ``BootstrapUtilsMixin`` class is added automatically to its plugin class in order to - enrich it with utility classes, such as :class:`cmsplugin_cascade.bootstrap4.mixins.BootstrapUtilities`. + enrich it with utility classes or html_attrs, such as :class:`cmsplugin_cascade.bootstrap4.mixins.BootstrapUtilities`. + If anchor_fields is specified in the property_fields attributes, these attribute choices are set when the request + is available whit id elements of the current page. """ + def __str__(self): return self.plugin_class.get_identifier(self) def get_form(self, request, obj=None, **kwargs): form = kwargs.get('form', self.form) + for anchors_field in self.fields_with_choices_anchors: + if hasattr(obj.page,'cascadepage'): + currentpage_element_ids = obj.page.cascadepage.glossary.get('element_ids', {}) + self.utility_form_mixin.base_fields[anchors_field].choices=[[items,value] for items, value in currentpage_element_ids.items()] + else: + self.utility_form_mixin.base_fields[anchors_field].choices=[] assert issubclass(form, EntangledModelFormMixin), "Form must inherit from EntangledModelFormMixin" kwargs['form'] = type(form.__name__, (self.utility_form_mixin, form), {}) 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) - for utility_field_name in cls.utility_form_mixin.base_fields.keys(): - css_class = obj.glossary.get(utility_field_name) - if css_class: - css_classes.append(css_class) + if 'css_classes' in cls.attr_type: + for utility_field_name in cls.attr_type['css_classes']: + css_class = obj.glossary.get(utility_field_name) + if css_class: + css_classes.append(css_class) return css_classes + + @classmethod + def get_html_tag_attributes(cls, obj): + """Enrich list of HTML attribute data with customized ones""" + attributes = super().get_html_tag_attributes(obj) + if 'html_data_attrs' in cls.attr_type: + for utility_field_name in cls.attr_type['html_data_attrs']: + attribute = obj.glossary.get(utility_field_name) + if attribute: + attributes.update({utility_field_name:attribute}) + return attributes diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 22f3cf259..46c3a10ab 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -2,6 +2,12 @@ Release History =============== +1.3 +=== +* Paste structure of placeholder to, and from Persisting Clipboard Content. +* Set links onto phone number. + + 1.2.3 ===== * Make page editor for extra fields configurable. diff --git a/docs/source/customize-styles.rst b/docs/source/customize-styles.rst index 804a8875a..9574cddcf 100644 --- a/docs/source/customize-styles.rst +++ b/docs/source/customize-styles.rst @@ -85,6 +85,10 @@ styles shall be applied. .. |customize-styles| image:: /_static/customize-styles.png +If the plugin has the default configuration, for example: sets the upper, lower, upper, and lower margins. +The default settings above will be available in the plug-in editor because CMSPLUGIN_CASCADE variable. +**merge_extra_fields**: True are set in defaults. + Allow ID -------- diff --git a/docs/source/link-plugin.rst b/docs/source/link-plugin.rst index a376b46e0..17ef65370 100644 --- a/docs/source/link-plugin.rst +++ b/docs/source/link-plugin.rst @@ -133,7 +133,7 @@ and :class:`cmsplugin_cascade.link.forms.LinkForm` by alternative implementation .. code-block:: python :caption: shop/cascade/plugin_base.py - from entangled.forms import get_related_object + from entangled.utils import get_related_object from cmsplugin_cascade.link.plugin_base import LinkPluginBase class CatalogLinkPluginBase(LinkPluginBase): diff --git a/examples/bs4demo/settings.py b/examples/bs4demo/settings.py index 8e8e3f130..26beb27bc 100644 --- a/examples/bs4demo/settings.py +++ b/examples/bs4demo/settings.py @@ -7,6 +7,8 @@ from django.urls import reverse_lazy from cmsplugin_cascade.extra_fields.config import PluginExtraFieldsConfig +from cmsplugin_cascade.bootstrap4.mixins import BootstrapUtilities +from cmsplugin_cascade.generic.mixins_html_attrs import GenericUtilities from django.utils.text import format_lazy DEBUG = True @@ -62,6 +64,7 @@ 'sass_processor', 'sekizai', 'bs4demo', + 'django_extensions', ] @@ -213,29 +216,56 @@ ) CMSPLUGIN_CASCADE = { - 'alien_plugins': ('TextPlugin', 'TextLinkPlugin',), + 'alien_plugins': ('TextPlugin', 'TextLinkPlugin','BootstrapListPlugin', 'BootstrapNavMainMemuPlugin'), 'plugins_with_sharables': { 'BootstrapImagePlugin': ('image_shapes', 'image_width_responsive', 'image_width_fixed', 'image_height', 'resize_options',), - 'BootstrapPicturePlugin': ('image_shapes', 'responsive_heights', 'image_size', 'resize_options',), + 'BootstrapPicturePlugin': ('image_shapes', 'responsive_heights', 'resize_options',), }, 'exclude_hiding_plugin': ('SegmentPlugin', 'Badge'), 'allow_plugin_hiding': True, 'leaflet': {'default_position': {'lat': 50.0, 'lng': 12.0, 'zoom': 6}}, 'cache_strides': True, + 'plugins_with_extra_fields': { + 'BootstrapColumnPlugin': PluginExtraFieldsConfig( + css_classes={'multiple': True, 'class_names': 'custom-zoom-over, custom-zoom-over2'}, + inline_styles={ + 'extra_fields:Border': ['border',], + 'extra_fields:Border Radius': ['border-radius'], + 'extra_units:Border Radius': 'px,rem', + 'extra_fields:Colors': ['color', 'background-color'], + 'extra_fields:Margins': ['margin-top', 'margin-right', 'margin-botton,', 'margin-left'], + 'extra_units:Margins': 'px,em', + 'extra_fields:Paddings': ['padding-top', 'padding-right', 'padding-botton,', 'padding-left'], + 'extra_units:Paddings': 'px,em', + }), + }, + 'plugins_with_extra_mixins': { + 'BootstrapColumnPlugin':BootstrapUtilities(BootstrapUtilities.background_and_color, BootstrapUtilities.vertical_margins, BootstrapUtilities.floats, BootstrapUtilities.paddings, BootstrapUtilities.margins, + GenericUtilities.scroll_animate ), + }, } +CASCADE_CLIPS_LIBRARY = True + CMS_PLACEHOLDER_CONF = { + + 'Header Content': { + 'plugins': ['BootstrapContainerPlugin', 'BootstrapJumbotronPlugin','BootstrapNavbarPlugin', 'BootstrapListPlugin', 'BootstrapNavItemsMainMemuPlugin'], + 'parent_classes': {'BootstrapContainerPlugin': None, 'BootstrapJumbotronPlugin': None, 'BootstrapNavItemsMainMemuPlugin':None}, + + }, + # this placeholder is used in templates/main.html, it shows how to # scaffold a djangoCMS page starting with an empty placeholder 'Main Content': { - 'plugins': ['BootstrapContainerPlugin', 'BootstrapJumbotronPlugin'], - 'parent_classes': {'BootstrapContainerPlugin': None, 'BootstrapJumbotronPlugin': None}, + 'plugins': ['BootstrapContainerPlugin', 'BootstrapJumbotronPlugin' , 'BootstrapListPlugin'], + 'parent_classes': {'BootstrapContainerPlugin': None, 'BootstrapJumbotronPlugin': None , }, }, # this placeholder is used in templates/wrapped.html, it shows how to # add content to an existing Bootstrap column 'Bootstrap Column': { - 'plugins': ['BootstrapRowPlugin', 'TextPlugin', ], + 'plugins': ['BootstrapRowPlugin', 'TextPlugin','BootstrapListPlugin' ], 'parent_classes': {'BootstrapRowPlugin': None}, 'require_parent': False, }, diff --git a/examples/bs4demo/templates/bs4demo/base.html b/examples/bs4demo/templates/bs4demo/base.html index 002ad408b..86d05577a 100644 --- a/examples/bs4demo/templates/bs4demo/base.html +++ b/examples/bs4demo/templates/bs4demo/base.html @@ -52,6 +52,26 @@

Services

{% endif %} + + + + {% if request.toolbar and request.toolbar.edit_mode_active %} + + {% endif %} {% render_block "js" %} diff --git a/examples/bs4demo/templates/bs4demo/main.html b/examples/bs4demo/templates/bs4demo/main.html index 51ae5935b..c68339dff 100644 --- a/examples/bs4demo/templates/bs4demo/main.html +++ b/examples/bs4demo/templates/bs4demo/main.html @@ -2,21 +2,34 @@ {% load static cms_tags bootstrap_tags sekizai_tags sass_tags %} {% block head %} -{% addtoblock "css" %}{% endaddtoblock %} +{% addtoblock "css" %} + + +{% endaddtoblock %} + {% endblock head %} {% block header %} - {% if DJANGO_CLIENT_FRAMEWORK == 'angular-ui' %} - {% include "bootstrap4/includes/ng-nav-navbar.html" with navbar_classes="navbar-expand-lg navbar-light bg-light fixed-top" %} - {% else %} - {% include "bootstrap4/includes/nav-navbar.html" with navbar_classes="navbar-expand-lg navbar-light bg-light fixed-top" role="navigation" %} + +{% static_placeholder 'Header Content' %} +{% if request.toolbar and request.toolbar.edit_mode_active %} +{% addtoblock "js" %} + +{% endaddtoblock %} {% endif %} -{% if cms_version >= "3.5.0" and request.toolbar %} - +{% if cms_version >= "3.5.0" and request.toolbar %} {% endif %} {% endblock header %} diff --git a/examples/manage.py b/examples/manage.py index 050490705..8584b3801 100755 --- a/examples/manage.py +++ b/examples/manage.py @@ -8,4 +8,6 @@ from django.core.management import execute_from_command_line os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bs4demo.settings') +# os.environ.setdefault('COMPACT_FORM', 'True') + execute_from_command_line(sys.argv) diff --git a/examples/package.json b/examples/package.json index ba87b90da..9d46e0606 100644 --- a/examples/package.json +++ b/examples/package.json @@ -11,14 +11,16 @@ "angular": "^1.5.11", "angular-animate": "^1.5.11", "angular-sanitize": "^1.5.11", - "ui-bootstrap4": "^3.0.5", "bootstrap": "^4.1.3", + "intersection-observer": "^0.7.0", "jquery": "^3.2.1", "leaflet": "^1.2.0", "leaflet-easybutton": "^2.2.0", "picturefill": "^3.0.2", "popper.js": "^1.12.9", - "select2": "^4.0.3" + "sal.js": "^0.6.5", + "select2": "^4.0.3", + "ui-bootstrap4": "^3.0.5" }, "devDependencies": {}, "scripts": { diff --git a/examples/poetry.lock b/examples/poetry.lock new file mode 100644 index 000000000..52f905dde --- /dev/null +++ b/examples/poetry.lock @@ -0,0 +1,656 @@ +[[package]] +category = "dev" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.11.28" + +[[package]] +category = "dev" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +name = "django" +optional = false +python-versions = ">=3.5" +version = "2.2.10" + +[package.dependencies] +pytz = "*" +sqlparse = "*" + +[package.extras] +argon2 = ["argon2-cffi (>=16.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +category = "dev" +description = "Generic drag-and-drop sorting for the List, the Stacked- and the Tabular-Inlines Views in the Django Admin" +name = "django-admin-sortable2" +optional = false +python-versions = "*" +version = "0.7.5" + +[package.dependencies] +Django = ">=1.8,<3.1" + +[[package]] +category = "dev" +description = "A helper class for handling configuration defaults of packaged apps gracefully." +name = "django-appconf" +optional = false +python-versions = "*" +version = "1.0.3" + +[package.dependencies] +django = "*" +six = "*" + +[[package]] +category = "dev" +description = "Class based template tags for Django" +name = "django-classy-tags" +optional = false +python-versions = "*" +version = "1.0.0" + +[package.dependencies] +django = ">=1.11" +six = "*" + +[[package]] +category = "dev" +description = "An Advanced Django CMS" +name = "django-cms" +optional = false +python-versions = "*" +version = "3.7.1" + +[package.dependencies] +Django = ">=1.11,<3.0" +django-classy-tags = ">=0.7.2" +django-formtools = ">=2.1" +django-sekizai = ">=0.7" +django-treebeard = ">=4.3" +djangocms-admin-style = ">=1.2" + +[[package]] +category = "dev" +description = "Compresses linked and inline JavaScript or CSS into single cached files." +name = "django-compressor" +optional = false +python-versions = "*" +version = "2.4" + +[package.dependencies] +django-appconf = ">=1.0.3" +rcssmin = "1.0.6" +rjsmin = "1.1.0" +six = ">=1.12.0" + +[[package]] +category = "dev" +description = "Dynamic global and instance settings for your django project" +name = "django-dynamic-preferences" +optional = false +python-versions = "*" +version = "1.8.1" + +[package.dependencies] +django = ">=1.11" +persisting-theory = ">=0.2.1" +six = "*" + +[[package]] +category = "dev" +description = "Edit JSON field using Django Model Form" +name = "django-entangled" +optional = false +python-versions = "*" +version = "0.3" + +[[package]] +category = "dev" +description = "A file management application for django that makes handling of files and images a breeze." +name = "django-filer" +optional = false +python-versions = "*" +version = "1.6.0" + +[package.dependencies] +Unidecode = ">=0.04,<1.1" +django = ">=1.11,<3.0" +django-mptt = ">=0.6,<1.0" +django_polymorphic = ">=0.7,<2.1" +easy-thumbnails = ">=2,<3.0" + +[[package]] +category = "dev" +description = "A set of high-level abstractions for Django forms" +name = "django-formtools" +optional = false +python-versions = "*" +version = "2.2" + +[package.dependencies] +Django = ">=1.11" + +[[package]] +category = "dev" +description = "script tag with additional attributes for django.forms.Media" +name = "django-js-asset" +optional = false +python-versions = "*" +version = "1.2.2" + +[[package]] +category = "dev" +description = "Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances." +name = "django-mptt" +optional = false +python-versions = ">=3.5" +version = "0.11.0" + +[package.dependencies] +Django = ">=1.11" +django-js-asset = "*" + +[[package]] +category = "dev" +description = "Seamless polymorphic inheritance for Django models" +name = "django-polymorphic" +optional = false +python-versions = "*" +version = "2.0.3" + +[package.dependencies] +Django = ">=1.11" + +[[package]] +category = "dev" +description = "SASS processor to compile SCSS files into *.css, while rendering, or offline." +name = "django-sass-processor" +optional = false +python-versions = "*" +version = "0.8" + +[package.extras] +dev = ["libsass (>=0.13)"] +management-command = ["django-compressor (>=2.4)"] + +[[package]] +category = "dev" +description = "Django Sekizai" +name = "django-sekizai" +optional = false +python-versions = "*" +version = "1.1.0" + +[package.dependencies] +django = ">=1.11" +django-classy-tags = ">=0.9.0" +six = "*" + +[[package]] +category = "dev" +description = "Select2 option fields for Django" +name = "django-select2" +optional = false +python-versions = "*" +version = "7.2.0" + +[package.dependencies] +django = ">=2.2" +django-appconf = ">=0.6.0" + +[[package]] +category = "dev" +description = "Efficient tree implementations for Django" +name = "django-treebeard" +optional = false +python-versions = "*" +version = "4.3.1" + +[package.dependencies] +Django = ">=1.8" + +[[package]] +category = "dev" +description = "Adds pretty CSS styles for the django CMS admin interface." +name = "djangocms-admin-style" +optional = false +python-versions = "*" +version = "1.5.0" + +[[package]] +category = "dev" +description = "Templates and templatetags to be used with django-CMS with Bootstrap3 or Bootstrap4." +name = "djangocms-bootstrap" +optional = false +python-versions = "*" +version = "1.1.2" + +[package.dependencies] +django-cms = ">3.4" + +[[package]] +category = "dev" +description = "Build Single Page Applications using the Django-CMS plugin system" +name = "djangocms-cascade" +optional = false +python-versions = "*" +version = "1.2.3" + +[package.dependencies] +django = ">=1.11,<3.0" +django-classy-tags = ">=0.8" +django-cms = ">=3.5,<4" +django-entangled = "*" +djangocms-text-ckeditor = ">=3.7" +jsonfield = "*" +requests = "*" + +[[package]] +category = "dev" +description = "Adds undo/redo functionality to django CMS" +name = "djangocms-history" +optional = false +python-versions = "*" +version = "1.1.0" + +[package.dependencies] +django-cms = ">=3.4.5" + +[[package]] +category = "dev" +description = "Text Plugin for django CMS with CKEditor support" +name = "djangocms-text-ckeditor" +optional = false +python-versions = "*" +version = "3.8.0" + +[package.dependencies] +Pillow = "*" +django-cms = ">=3.4.5" +html5lib = ">=0.999999999" + +[[package]] +category = "dev" +description = "Adds import and export of plugin data." +name = "djangocms-transfer" +optional = false +python-versions = "*" +version = "0.2.0" + +[package.dependencies] +django-cms = ">=3.4.5" + +[[package]] +category = "dev" +description = "Easy thumbnails for Django" +name = "easy-thumbnails" +optional = false +python-versions = ">=3.5" +version = "2.7" + +[package.dependencies] +django = ">=1.11,<4.0" +pillow = "*" + +[[package]] +category = "dev" +description = "HTML parser based on the WHATWG HTML specification" +name = "html5lib" +optional = false +python-versions = "*" +version = "1.0.1" + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["genshi", "chardet (>=2.2)", "datrie", "lxml"] +chardet = ["chardet (>=2.2)"] +datrie = ["datrie"] +genshi = ["genshi"] +lxml = ["lxml"] + +[[package]] +category = "dev" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "dev" +description = "A reusable Django field that allows you to store validated JSON in your model." +name = "jsonfield" +optional = false +python-versions = "*" +version = "3.0.0" + +[package.dependencies] +Django = ">=2.2" + +[[package]] +category = "dev" +description = "Sass for Python: A straightforward binding of libsass for Python." +name = "libsass" +optional = false +python-versions = "*" +version = "0.19.4" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "Registries that can autodiscover values accross your project apps" +name = "persisting-theory" +optional = false +python-versions = "*" +version = "0.2.1" + +[[package]] +category = "dev" +description = "Python Imaging Library (Fork)" +name = "pillow" +optional = false +python-versions = ">=3.5" +version = "7.0.0" + +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2019.3" + +[[package]] +category = "dev" +description = "CSS Minifier" +name = "rcssmin" +optional = false +python-versions = "*" +version = "1.0.6" + +[[package]] +category = "dev" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "dev" +description = "Javascript Minifier" +name = "rjsmin" +optional = false +python-versions = "*" +version = "1.1.0" + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" + +[[package]] +category = "dev" +description = "Non-validating SQL parser" +name = "sqlparse" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.3.0" + +[[package]] +category = "dev" +description = "ASCII transliterations of Unicode text" +name = "unidecode" +optional = false +python-versions = "*" +version = "1.0.23" + +[[package]] +category = "dev" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.8" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "dev" +description = "Character encoding aliases for legacy web content" +name = "webencodings" +optional = false +python-versions = "*" +version = "0.5.1" + +[metadata] +content-hash = "2115171a195bcabffe11d3f5f9b97eb118bbc02c9c77c45ed3c7c68db1550ff6" +python-versions = "^3.6" + +[metadata.files] +certifi = [ + {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, + {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +django = [ + {file = "Django-2.2.10-py3-none-any.whl", hash = "sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038"}, + {file = "Django-2.2.10.tar.gz", hash = "sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a"}, +] +django-admin-sortable2 = [ + {file = "django-admin-sortable2-0.7.5.tar.gz", hash = "sha256:1366cb159439aa28472639dc1f33d6370a56d4c3efbe3def854d953aaa6bbd24"}, +] +django-appconf = [ + {file = "django-appconf-1.0.3.tar.gz", hash = "sha256:35f13ca4d567f132b960e2cd4c832c2d03cb6543452d34e29b7ba10371ba80e3"}, + {file = "django_appconf-1.0.3-py2.py3-none-any.whl", hash = "sha256:c98a7af40062e996b921f5962a1c4f3f0c979fa7885f7be4710cceb90ebe13a6"}, +] +django-classy-tags = [ + {file = "django-classy-tags-1.0.0.tar.gz", hash = "sha256:ad6a25fc2b58a098f00d86bd5e5dad47922f5ca4e744bc3cccb7b4be5bc35eb1"}, +] +django-cms = [ + {file = "django-cms-3.7.1.tar.gz", hash = "sha256:05ea915d490562413428e04acb9ecae604c63b8dc5ed9250d0e9437b1314c996"}, + {file = "django_cms-3.7.1-py2.py3-none-any.whl", hash = "sha256:89d1e3bebd732a6bd00b0cfaf8128bfb0924429e83f1297e0b34dd9343e55b7a"}, +] +django-compressor = [ + {file = "django_compressor-2.4-py2.py3-none-any.whl", hash = "sha256:57ac0a696d061e5fc6fbc55381d2050f353b973fb97eee5593f39247bc0f30af"}, + {file = "django_compressor-2.4.tar.gz", hash = "sha256:d2ed1c6137ddaac5536233ec0a819e14009553fee0a869bea65d03e5285ba74f"}, +] +django-dynamic-preferences = [ + {file = "django-dynamic-preferences-1.8.1.tar.gz", hash = "sha256:727f71f865ff29df93c01a0a1686af0dca19776ee786f876ea0ae7e062719b24"}, + {file = "django_dynamic_preferences-1.8.1-py2.py3-none-any.whl", hash = "sha256:1d600c2f88baa9e0098e856f8a14e320a4a479305deea1afbc8722d953c44c65"}, +] +django-entangled = [ + {file = "django-entangled-0.3.tar.gz", hash = "sha256:e51cb1748f30d3dc474fd32d195f051307507a3dbbe789f0fed29b80ce2e6a35"}, +] +django-filer = [ + {file = "django-filer-1.6.0.tar.gz", hash = "sha256:3f2045cfd9e53c1a29cd8a71747e984faead630ee72baab29d6b3b45584d52e0"}, +] +django-formtools = [ + {file = "django-formtools-2.2.tar.gz", hash = "sha256:c5272c03c1cd51b2375abf7397a199a3148a9fbbf2f100e186467a84025d13b2"}, + {file = "django_formtools-2.2-py2.py3-none-any.whl", hash = "sha256:304fa777b8ef9e0693ce7833f885cb89ba46b0e46fc23b01176900a93f46742f"}, +] +django-js-asset = [ + {file = "django-js-asset-1.2.2.tar.gz", hash = "sha256:c163ae80d2e0b22d8fb598047cd0dcef31f81830e127cfecae278ad574167260"}, + {file = "django_js_asset-1.2.2-py2.py3-none-any.whl", hash = "sha256:8ec12017f26eec524cab436c64ae73033368a372970af4cf42d9354fcb166bdd"}, +] +django-mptt = [ + {file = "django-mptt-0.11.0.tar.gz", hash = "sha256:dfdb3af75ad27cdd4458b0544ec8574174f2b90f99bc2cafab6a15b4bc1895a8"}, + {file = "django_mptt-0.11.0-py2.py3-none-any.whl", hash = "sha256:90eb236eb4f1a92124bd7c37852bbe09c0d21158477cc237556d59842a91c509"}, +] +django-polymorphic = [ + {file = "django-polymorphic-2.0.3.tar.gz", hash = "sha256:1fb5505537bcaf71cfc951ff94c4e3ba83c761eaca04b7b2ce9cb63937634ea5"}, + {file = "django_polymorphic-2.0.3-py2.py3-none-any.whl", hash = "sha256:79e7df455fdc8c3d28d38b7ab8323fc21d109a162b8ca282119e0e9ce8db7bdb"}, +] +django-sass-processor = [ + {file = "django-sass-processor-0.8.tar.gz", hash = "sha256:e039551994feaaba6fcf880412b25a772dd313162a34cbb4289814988cfae340"}, +] +django-sekizai = [ + {file = "django-sekizai-1.1.0.tar.gz", hash = "sha256:e2f6e666d4dd9d3ecc27284acb85ef709e198014f5d5af8c6d54ed04c2d684d9"}, +] +django-select2 = [ + {file = "django-select2-7.2.0.tar.gz", hash = "sha256:4c531cb7e9eb4152c7e5f8ab83be386f46978b3d80e91e55ad1fb46382222a0b"}, + {file = "django_select2-7.2.0-py2.py3-none-any.whl", hash = "sha256:d17bb0e64503a7e52ba405f73a187664906cefda5f1c33971c67ab0b3891e91c"}, +] +django-treebeard = [ + {file = "django-treebeard-4.3.1.tar.gz", hash = "sha256:83aebc34a9f06de7daaec330d858d1c47887e81be3da77e3541fe7368196dd8a"}, +] +djangocms-admin-style = [ + {file = "djangocms-admin-style-1.5.0.tar.gz", hash = "sha256:47d9868aa448e593ce90059d6542a4d907c120d357cd0561c586f5557e3ba316"}, +] +djangocms-bootstrap = [ + {file = "djangocms-bootstrap-1.1.2.tar.gz", hash = "sha256:60e0802111df17298933104fdc73fc7bb74924e435d5c025b3a12a29a10e6761"}, +] +djangocms-cascade = [ + {file = "djangocms-cascade-1.2.3.tar.gz", hash = "sha256:5fb0e6519d36577469bbbd6645c41220db0abade147fe6e0dc530c02b845755a"}, +] +djangocms-history = [ + {file = "djangocms-history-1.1.0.tar.gz", hash = "sha256:783c0aea25671e14df634838660e53023d6559c66697c9d8d8cfa771842e9ff2"}, +] +djangocms-text-ckeditor = [ + {file = "djangocms-text-ckeditor-3.8.0.tar.gz", hash = "sha256:0f0291cdf305c469741a639d89c71ee77f29dfc5aada4f7a453d6dc2926ceca9"}, +] +djangocms-transfer = [ + {file = "djangocms-transfer-0.2.0.tar.gz", hash = "sha256:9e7d251a82fa13beb6bf541a157a63b8306a8ae29a90349c783f0077e439f325"}, +] +easy-thumbnails = [ + {file = "easy-thumbnails-2.7.tar.gz", hash = "sha256:e4e7a0dd4001f56bfd4058428f2c91eafe27d33ef3b8b33ac4e013b159b9ff91"}, +] +html5lib = [ + {file = "html5lib-1.0.1-py2.py3-none-any.whl", hash = "sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3"}, + {file = "html5lib-1.0.1.tar.gz", hash = "sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"}, +] +idna = [ + {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, + {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, +] +jsonfield = [ + {file = "jsonfield-3.0.0-py3-none-any.whl", hash = "sha256:a6616f38db42542d1f2434f5eae1a91550e5cc15dff27edd87b4358493ef2a92"}, + {file = "jsonfield-3.0.0.tar.gz", hash = "sha256:1a17efe9a26850e9175900cb37cf30c6182fc2251989f8af44ef47fbbf48eaff"}, +] +libsass = [ + {file = "libsass-0.19.4-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32"}, + {file = "libsass-0.19.4-cp27-cp27m-win32.whl", hash = "sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f"}, + {file = "libsass-0.19.4-cp27-cp27m-win_amd64.whl", hash = "sha256:4dcfd561fb100250b89496e1362b96f2cc804f689a59731eb0f94f9a9e144f4a"}, + {file = "libsass-0.19.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:845a9573b25c141164972d498855f4ad29367c09e6d76fad12955ad0e1c83013"}, + {file = "libsass-0.19.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:81a013a4c2a614927fd1ef7a386eddabbba695cbb02defe8f31cf495106e974c"}, + {file = "libsass-0.19.4-cp35-cp35m-win32.whl", hash = "sha256:fcb7ab4dc81889e5fc99cafbc2017bc76996f9992fc6b175f7a80edac61d71df"}, + {file = "libsass-0.19.4-cp35-cp35m-win_amd64.whl", hash = "sha256:fc5f8336750f76f1bfae82f7e9e89ae71438d26fc4597e3ab4c05ca8fcd41d8a"}, + {file = "libsass-0.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9b59afa0d755089c4165516400a39a289b796b5612eeef5736ab7a1ebf96a67c"}, + {file = "libsass-0.19.4-cp36-cp36m-win32.whl", hash = "sha256:c93df526eeef90b1ea4799c1d33b6cd5aea3e9f4633738fb95c1287c13e6b404"}, + {file = "libsass-0.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0fd8b4337b3b101c6e6afda9112cc0dc4bacb9133b59d75d65968c7317aa3272"}, + {file = "libsass-0.19.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:003a65b4facb4c5dbace53fb0f70f61c5aae056a04b4d112a198c3c9674b31f2"}, + {file = "libsass-0.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:338e9ae066bf1fde874e335324d5355c52d2081d978b4f74fc59536564b35b08"}, + {file = "libsass-0.19.4-cp37-cp37m-win32.whl", hash = "sha256:e318f06f06847ff49b1f8d086ac9ebce1e63404f7ea329adab92f4f16ba0e00e"}, + {file = "libsass-0.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a7e685466448c9b1bf98243339793978f654a1151eb5c975f09b83c7a226f4c1"}, + {file = "libsass-0.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a51393d75f6e3c812785b0fa0b7d67c54258c28011921f204643b55f7355ec0"}, + {file = "libsass-0.19.4.tar.gz", hash = "sha256:8b5b6d1a7c4ea1d954e0982b04474cc076286493f6af2d0a13c2e950fbe0be95"}, +] +persisting-theory = [ + {file = "persisting-theory-0.2.1.tar.gz", hash = "sha256:00ff7dcc8f481ff75c770ca5797d968e8725b6df1f77fe0cf7d20fa1e5790c0a"}, +] +pillow = [ + {file = "Pillow-7.0.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00"}, + {file = "Pillow-7.0.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff"}, + {file = "Pillow-7.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865"}, + {file = "Pillow-7.0.0-cp35-cp35m-win32.whl", hash = "sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386"}, + {file = "Pillow-7.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435"}, + {file = "Pillow-7.0.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2"}, + {file = "Pillow-7.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317"}, + {file = "Pillow-7.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2"}, + {file = "Pillow-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313"}, + {file = "Pillow-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0"}, + {file = "Pillow-7.0.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f"}, + {file = "Pillow-7.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636"}, + {file = "Pillow-7.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9"}, + {file = "Pillow-7.0.0-cp37-cp37m-win32.whl", hash = "sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837"}, + {file = "Pillow-7.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda"}, + {file = "Pillow-7.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be"}, + {file = "Pillow-7.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533"}, + {file = "Pillow-7.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614"}, + {file = "Pillow-7.0.0-cp38-cp38-win32.whl", hash = "sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a"}, + {file = "Pillow-7.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d"}, + {file = "Pillow-7.0.0-pp373-pypy36_pp73-win32.whl", hash = "sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358"}, + {file = "Pillow-7.0.0.tar.gz", hash = "sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946"}, +] +pytz = [ + {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, + {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, +] +rcssmin = [ + {file = "rcssmin-1.0.6.tar.gz", hash = "sha256:ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"}, +] +requests = [ + {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, + {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, +] +rjsmin = [ + {file = "rjsmin-1.1.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:799890bd07a048892d8d3deb9042dbc20b7f5d0eb7da91e9483c561033b23ce2"}, + {file = "rjsmin-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:211c2fe8298951663bbc02acdffbf714f6793df54bfc50e1c6c9e71b3f2559a3"}, + {file = "rjsmin-1.1.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:585e75a84d9199b68056fd4a083d9a61e2a92dfd10ff6d4ce5bdb04bc3bdbfaf"}, + {file = "rjsmin-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e487a7783ac4339e79ec610b98228eb9ac72178973e3dee16eba0e3feef25924"}, + {file = "rjsmin-1.1.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:0ab825839125eaca57cc59581d72e596e58a7a56fbc0839996b7528f0343a0a8"}, + {file = "rjsmin-1.1.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:6044ca86e917cd5bb2f95e6679a4192cef812122f28ee08c677513de019629b3"}, + {file = "rjsmin-1.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ecd29f1b3e66a4c0753105baec262b331bcbceefc22fbe6f7e8bcd2067bcb4d7"}, + {file = "rjsmin-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:975b69754d6a76be47c0bead12367a1ca9220d08e5393f80bab0230d4625d1f4"}, + {file = "rjsmin-1.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:466fe70cc5647c7c51b3260c7e2e323a98b2b173564247f9c89e977720a0645f"}, + {file = "rjsmin-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e3908b21ebb584ce74a6ac233bdb5f29485752c9d3be5e50c5484ed74169232c"}, + {file = "rjsmin-1.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:714329db774a90947e0e2086cdddb80d5e8c4ac1c70c9f92436378dedb8ae345"}, + {file = "rjsmin-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dd0f4819df4243ffe4c964995794c79ca43943b5b756de84be92b445a652fb86"}, + {file = "rjsmin-1.1.0.tar.gz", hash = "sha256:b15dc75c71f65d9493a8c7fa233fdcec823e3f1b88ad84a843ffef49b338ac32"}, +] +six = [ + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, +] +sqlparse = [ + {file = "sqlparse-0.3.0-py2.py3-none-any.whl", hash = "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177"}, + {file = "sqlparse-0.3.0.tar.gz", hash = "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"}, +] +unidecode = [ + {file = "Unidecode-1.0.23-py2.py3-none-any.whl", hash = "sha256:092cdf7ad9d1052c50313426a625b717dab52f7ac58f859e09ea020953b1ad8f"}, + {file = "Unidecode-1.0.23.tar.gz", hash = "sha256:8b85354be8fd0c0e10adbf0675f6dc2310e56fda43fa8fe049123b6c475e52fb"}, +] +urllib3 = [ + {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, + {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] diff --git a/examples/pyproject.toml b/examples/pyproject.toml index e805b01a8..2a1a6afe9 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -11,6 +11,7 @@ python = "^3.6" [tool.poetry.dev-dependencies] django = "<2.3" djangocms-cascade = "*" +django-entangled = { git = "https://github.com/jrief/django-entangled.git", branch="develop" } django-compressor = "*" django-filer = "*" django-sass-processor = "*" diff --git a/setup.py b/setup.py index 9d0f8f1be..84e3f82eb 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ 'django>=1.11,<3.0', 'django-classy-tags>=0.8', 'django-cms>=3.5,<4', - 'django-entangled', + # 'django-entangled', 'djangocms-text-ckeditor>=3.7', 'jsonfield', 'requests', diff --git a/tests/bootstrap4/test_accordion.py b/tests/bootstrap4/test_accordion.py index bef9c5fdf..d775ed0af 100644 --- a/tests/bootstrap4/test_accordion.py +++ b/tests/bootstrap4/test_accordion.py @@ -59,3 +59,4 @@ def test_edit_accordion_group(rf, admin_site, bootstrap_accordion):
""".format(accordion_id=accordion_model.id, group_id=group_model.id) expected = expected.replace('\n', '').replace('\t', '') assert html == expected + diff --git a/tests/requirements.txt b/tests/requirements.txt index 9f03e2295..23c864578 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,3 @@ -lxml==3.5.0 pluggy==0.12.0 py==1.8.0 pytest==4.5.0 @@ -8,3 +7,5 @@ virtualenv==13.1.2 django-reversion==3.0.3 factory-boy==2.12.0 pytest-factoryboy==2.0.3 +django-entangled +#https://github.com/jrief/django-entangled/archive/develop.zip diff --git a/tests/settings.py b/tests/settings.py index bb8e76421..0369e7ecc 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -94,7 +94,8 @@ 'treebeard', 'menus', 'sekizai', - 'cms', + 'cms', + 'cms_bootstrap', 'adminsortable2', 'djangocms_text_ckeditor', 'django_select2', diff --git a/tests/static/strides/bootstrap-jumbotron-navbar.json b/tests/static/strides/bootstrap-jumbotron-navbar.json new file mode 100644 index 000000000..8d74191b5 --- /dev/null +++ b/tests/static/strides/bootstrap-jumbotron-navbar.json @@ -0,0 +1,100 @@ +{ + "plugins":[ + [ + "BootstrapJumbotronPlugin", + { + "glossary":{ + "hide_plugin":false, + "Position":"fixed-top", + "fluid":true, + "background_color":[ + "#808080", + true + ], + "element_heights_choices":"auto", + "element_heights":{ + "xs":"1rem", + "sm":"1rem", + "md":"1rem", + "lg":"1rem", + "xl":"1rem" + }, + "image_file":{ + "model":"filer.image", + "pk":24 + }, + "background_repeat":"no-repeat", + "background_attachment":"local", + "background_vertical_position":"center", + "background_horizontal_position":"center", + "background_size":"cover", + "background_width_height":{}, + "media_queries":{ + "xs":{ + "width":576, + "media":"(max-width: 575.98px)" + }, + "sm":{ + "width":768, + "media":"(min-width: 576px) and (max-width: 767.98px)" + }, + "md":{ + "width":992, + "media":"(min-width: 768px) and (max-width: 991.98px)" + }, + "lg":{ + "width":1200, + "media":"(min-width: 992px) and (max-width: 1199.98px)" + }, + "xl":{ + "width":1980, + "media":"(min-width: 1200px)" + } + } + }, + "pk":2498 + }, + [ + [ + "BootstrapNavbarPlugin", + { + "glossary":{ + "hide_plugin":false, + "extra_inline_styles:color":[ + "#808080", + false + ], + "extra_inline_styles:background-color":[ + "#808080", + false + ], + "navbar_collapse":"navbar-expand-lg", + "navbar_color":"inherit", + "navbar_bg_color":"inherit", + "navbar_placement":"inherit", + "position_jumbotron":"fixed-top", + "fluid":false + }, + "pk":2499 + }, + [ + [ + "BootstrapNavCollapsePlugin", + { + "glossary":{ + "hide_plugin":false, + "Justify_content_xs":"", + "Justify_content_sm":"", + "Justify_content_md":"", + "Justify_content_lg":"" + }, + "pk":2500 + }, + [] + ] + ] + ] + ] + ] + ] +} diff --git a/tests/static/strides/bootstrap-navbar.json b/tests/static/strides/bootstrap-navbar.json new file mode 100644 index 000000000..86d199fe5 --- /dev/null +++ b/tests/static/strides/bootstrap-navbar.json @@ -0,0 +1,192 @@ +{ + "plugins":[ + [ + "BootstrapNavbarPlugin", + { + "glossary":{ + "hide_plugin":false, + "extra_inline_styles:color":[ + "#808080", + true + ], + "extra_inline_styles:background-color":[ + "#808080", + true + ], + "navbar_collapse":"navbar-expand-md", + "navbar_color":"navbar-light", + "navbar_bg_color":"bg-transparent", + "navbar_placement":"inherit", + "position_jumbotron":"inherit" + }, + "pk":2701 + }, + [ + [ + "BootstrapNavBrandPlugin", + { + "glossary":{ + "hide_plugin":false, + "background_and_color":"" + }, + "pk":2702 + }, + [ + [ + "BootstrapNavBrandImagePlugin", + { + "glossary":{ + "hide_plugin":false, + "image_file":{ + "model":"filer.image", + "pk":1 + }, + "image_title":"", + "alt_tag":"", + "_image_properties":{ + "width":189, + "height":166, + "exif_orientation":1 + }, + "image_width_fixed":"90px" + }, + "pk":2703 + }, + [] + ] + ] + ], + [ + "BootstrapNavbarToogler", + { + "glossary":{ + "hide_plugin":false, + "background_and_color":"" + }, + "pk":2704 + }, + [] + ], + [ + "BootstrapNavCollapsePlugin", + { + "glossary":{ + "hide_plugin":false, + "Justify_content_xs":"justify-content-start", + "Justify_content_sm":"", + "Justify_content_md":"", + "Justify_content_lg":"" + }, + "pk":2705 + }, + [ + [ + "BootstrapListsPlugin", + { + "glossary":{ + "hide_plugin":false, + "extra_inline_styles:color":[ + "#808080", + true + ], + "extra_inline_styles:background-color":[ + "#808080", + true + ], + "Flex_xs":"", + "Flex_sm":"", + "Flex_md":"", + "Flex_lg":"", + "margins_xs":"", + "margins_sm":"", + "margins_md":"", + "margins_lg":"", + "padding_xs":"pl-1", + "padding_sm":"", + "padding_md":"", + "padding_lg":"", + "background_and_color":"", + "list_options":"navbar-nav", + "child_css_classes":"nav-item nav-item" + }, + "pk":2706 + }, + [ + [ + "BootstrapNavItemsMainMenuPlugin", + { + "glossary":{ + "hide_plugin":false + }, + "pk":2707 + }, + [] + ], + [ + "TextPlugin", + { + "body":"

", + "pk":2708 + }, + [ + [ + "TextLinkPlugin", + { + "glossary":{ + "render_template":"cascade/link/text-link.html", + "link_type":"cmspage", + "cms_page":{ + "model":"cms.page", + "pk":2 + }, + "section":"", + "download_file":null, + "ext_url":"", + "mail_to":"", + "link_target":"", + "link_title":"", + "link_content":"extern_link" + }, + "pk":2709 + }, + [] + ] + ] + ], + [ + "BootstrapButtonPlugin", + { + "glossary":{ + "hide_plugin":false, + "link_type":"", + "cms_page":null, + "section":"", + "download_file":null, + "ext_url":"", + "mail_to":"", + "link_target":"", + "link_title":"", + "icon_font":{ + "model":"cmsplugin_cascade.iconfont", + "pk":1 + }, + "symbol":"", + "link_content":"extern_link_btn", + "button_type":"btn-info", + "button_size":"", + "button_options":[], + "icon_align":"icon-right", + "stretched_link":false + }, + "pk":2710 + }, + [] + ] + ] + ] + ] + ] + ] + ] + ] +} diff --git a/tests/test_iconfont.py b/tests/test_iconfont.py index 5872c9e15..5a7ca58a6 100644 --- a/tests/test_iconfont.py +++ b/tests/test_iconfont.py @@ -13,7 +13,7 @@ from cms.plugin_rendering import ContentRenderer from cmsplugin_cascade.models import CascadeElement, IconFont from cmsplugin_cascade.icon.forms import IconFormMixin -from cmsplugin_cascade.icon.cms_plugins import SimpleIconPlugin +from cmsplugin_cascade.icon.simpleicon import SimpleIconPlugin from .conftest import UserFactory diff --git a/tests/test_strides.py b/tests/test_strides.py index b0d86291b..ff7b83d9c 100644 --- a/tests/test_strides.py +++ b/tests/test_strides.py @@ -13,6 +13,8 @@ from django.template import RequestContext, Template from django.test import RequestFactory +from django.contrib.auth.models import AnonymousUser + from cmsplugin_cascade.models import IconFont from filer.admin.clipboardadmin import ajax_upload @@ -22,7 +24,8 @@ class StridePluginTest(CascadeTestCase): def setUp(self): request = RequestFactory().get('/') - self.context = RequestContext(request, {}) + request.user = AnonymousUser() + self.context = RequestContext(request, {}) def assertStyleEqual(self, provided, expected): styles = dict((pair.split(':')[0].strip(), pair.split(':')[1].strip()) @@ -128,7 +131,14 @@ def test_carousel_plugin(self): self.assertListEqual(carousel.ol.attrs['class'], ['carousel-indicators']) self.assertListEqual(carousel.ol.li.attrs['class'], ['active']) slide = carousel.find(class_='carousel-inner') - self.assertSetEqual(set(slide.div.attrs['class']), {'carousel-item', 'active'}) + self.assertSetEqual(set(slide.div.attrs['class']), {'carousel-item', 'active'}) + + def test_navbar_plugin(self): + template = Template('{% load cascade_tags %}{% render_cascade "strides/bootstrap-navbar.json" %}') + html = template.render(self.context) + soup = BeautifulSoup(html, features='lxml') + navbar = soup.find(class_='navbar') + self.assertSetEqual(set( navbar.attrs['class']), {'navbar', 'navbar-expand-md','navbar-light','bg-transparent' }) def test_button_plugin(self): template = Template('{% load cascade_tags %}{% render_cascade "strides/bootstrap-button.json" %}') diff --git a/tox.ini b/tox.ini index 631d264a7..d08dd207e 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,8 @@ deps = django22: Django<3.0 django111: Django-Select2<6 django{20,21,22}: Django-Select2 + py{35,36}: lxml==3.5.0 + py{37}: lxml==4.5.0 -r requirements/base.txt -r tests/requirements.txt beautifulsoup4==4.8.1