diff --git a/cmsplugin_cascade/app_settings.py b/cmsplugin_cascade/app_settings.py index 6618a3600..e1db86b6c 100644 --- a/cmsplugin_cascade/app_settings.py +++ b/cmsplugin_cascade/app_settings.py @@ -58,7 +58,7 @@ def CMSPLUGIN_CASCADE(self): if 'cmsplugin_cascade.sharable' in INSTALLED_APPS: config['plugins_with_sharables'].setdefault( 'FramedIconPlugin', - ['font_size', 'color', 'background_color', 'text_align', 'border', 'border_radius']) + ['icon_nested.font_size', 'icon_nested.color', 'icon_nested.background_color', 'icon_nested.text_align', 'icon_nested.border', 'icon_nested.border_radius']) config['exclude_hiding_plugin'] = list(config.get('exclude_hiding_plugin', [])) config['exclude_hiding_plugin'].append('SegmentPlugin') diff --git a/cmsplugin_cascade/bootstrap4/accordion.py b/cmsplugin_cascade/bootstrap4/accordion.py index 50d0d6743..d5d0f3327 100644 --- a/cmsplugin_cascade/bootstrap4/accordion.py +++ b/cmsplugin_cascade/bootstrap4/accordion.py @@ -4,15 +4,15 @@ from django.utils.safestring import mark_safe from django.utils.text import Truncator from django.utils.html import escape -from entangled.forms import EntangledModelFormMixin +from entangled.forms import EntangledModelFormMixin, EntangledFormField, EntangledForm from cms.plugin_pool import plugin_pool -from cmsplugin_cascade.forms import ManageChildrenFormMixin +from cmsplugin_cascade.forms import ManageChildrenFormMixin, ManageNestedFormMixin from cmsplugin_cascade.plugin_base import TransparentWrapper, TransparentContainer from cmsplugin_cascade.widgets import NumberInputWidget from .plugin_base import BootstrapPluginBase -class AccordionFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): +class AccordionForm(EntangledForm): num_children = IntegerField( min_value=1, initial=1, @@ -35,9 +35,12 @@ class AccordionFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): help_text=_("Start with the first card open.") ) +class AccordionFormMixin(EntangledModelFormMixin, ManageNestedFormMixin): + accordion_nested = EntangledFormField(AccordionForm) + class Meta: - untangled_fields = ['num_children'] - entangled_fields = {'glossary': ['close_others', 'first_is_open']} + untangled_fields = ['accordion_nested.num_children'] + entangled_fields = {'glossary': ['accordion_nested']} class BootstrapAccordionPlugin(TransparentWrapper, BootstrapPluginBase): @@ -59,20 +62,20 @@ def get_identifier(cls, obj): def render(self, context, instance, placeholder): context = self.super(BootstrapAccordionPlugin, self).render(context, instance, placeholder) context.update({ - 'close_others': instance.glossary.get('close_others', True), - 'first_is_open': instance.glossary.get('first_is_open', True), + 'close_others': instance.glossary.get('accordion_nested', {}).get('close_others', True), + 'first_is_open': instance.glossary.get('accordion_nested', {}).get('first_is_open', True), }) return context - def save_model(self, request, obj, form, change): - wanted_children = int(form.cleaned_data.get('num_children')) + def save_model(self, request, obj, form, change): + wanted_children = int(form.cleaned_data.get('accordion_nested.num_children')) super().save_model(request, obj, form, change) self.extend_children(obj, wanted_children, BootstrapAccordionGroupPlugin) plugin_pool.register_plugin(BootstrapAccordionPlugin) -class AccordionGroupFormMixin(EntangledModelFormMixin): +class AccordionGroupForm(EntangledForm): heading = CharField( label=_("Heading"), widget=widgets.TextInput(attrs={'size': 80}), @@ -85,11 +88,15 @@ class AccordionGroupFormMixin(EntangledModelFormMixin): help_text=_("Add standard padding to card body."), ) - class Meta: - entangled_fields = {'glossary': ['heading', 'body_padding']} - def clean_heading(self): - return escape(self.cleaned_data['heading']) + return escape(self.cleaned_data.get('heading')) + + +class AccordionGroupFormMixin( ManageNestedFormMixin, EntangledModelFormMixin): + accordion_nested = EntangledFormField(AccordionGroupForm) + + class Meta: + entangled_fields = {'glossary': ['accordion_nested']} class BootstrapAccordionGroupPlugin(TransparentContainer, BootstrapPluginBase): @@ -102,15 +109,16 @@ class BootstrapAccordionGroupPlugin(TransparentContainer, BootstrapPluginBase): @classmethod def get_identifier(cls, instance): - heading = instance.glossary.get('heading', '') + heading = instance.glossary.get('accordion_nested', {}).get('heading' , '') return Truncator(heading).words(3, truncate=' ...') def render(self, context, instance, placeholder): context = self.super(BootstrapAccordionGroupPlugin, self).render(context, instance, placeholder) context.update({ - 'heading': mark_safe(instance.glossary.get('heading', '')), - 'no_body_padding': not instance.glossary.get('body_padding', True), + 'heading': mark_safe(instance.glossary.get('accordion_nested', {}).get('heading' , '')), + 'no_body_padding': not instance.glossary.get('accordion_nested', {}).get('no_body_padding' , True), }) return context plugin_pool.register_plugin(BootstrapAccordionGroupPlugin) + diff --git a/cmsplugin_cascade/bootstrap4/buttons.py b/cmsplugin_cascade/bootstrap4/buttons.py index 282d74cb6..2b4e43775 100644 --- a/cmsplugin_cascade/bootstrap4/buttons.py +++ b/cmsplugin_cascade/bootstrap4/buttons.py @@ -3,13 +3,13 @@ from django.forms.fields import BooleanField, CharField, ChoiceField, MultipleChoiceField from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ -from entangled.forms import EntangledModelFormMixin +from entangled.forms import EntangledModelFormMixin, EntangledFormField, EntangledForm from cms.plugin_pool import plugin_pool from cmsplugin_cascade.icon.plugin_base import IconPluginMixin from cmsplugin_cascade.icon.forms import IconFormMixin from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin from cmsplugin_cascade.link.plugin_base import LinkElementMixin - +from cmsplugin_cascade.forms import ManageChildrenFormMixin, ManageNestedFormMixin class ButtonTypeWidget(widgets.RadioSelect): """ @@ -24,8 +24,7 @@ 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): +class ButtonForm(EntangledForm): BUTTON_TYPES = [ ('btn-primary', _("Primary")), ('btn-secondary', _("Secondary")), @@ -93,6 +92,9 @@ class ButtonFormMixin(EntangledModelFormMixin): "relative parent, perfect for entirely clickable cards!") ) +class ButtonFormMixin(EntangledModelFormMixin, ManageNestedFormMixin): + button_opts_nested = EntangledFormField(ButtonForm) + icon_align = ChoiceField( label=_("Icon alignment"), choices=[ @@ -105,8 +107,7 @@ class ButtonFormMixin(EntangledModelFormMixin): ) class Meta: - entangled_fields = {'glossary': ['link_content', 'button_type', 'button_size', 'button_options', 'icon_align', - 'stretched_link']} + entangled_fields = {'glossary': ['button_opts_nested', 'icon_align',]} class BootstrapButtonMixin(IconPluginMixin): @@ -154,7 +155,7 @@ class Media: @classmethod def get_identifier(cls, instance): - content = instance.glossary.get('link_content') + content = instance.glossary.get('button_opts_nested', {}).get('link_content') if not content: try: button_types = dict(ButtonFormMixin.BUTTON_TYPES) @@ -166,7 +167,7 @@ def get_identifier(cls, instance): @classmethod def get_css_classes(cls, obj): css_classes = cls.super(BootstrapButtonPlugin, cls).get_css_classes(obj) - if obj.glossary.get('stretched_link'): + if obj.glossary.get('button_opts_nested', {}).get('stretched_link'): css_classes.append('stretched_link') return css_classes diff --git a/cmsplugin_cascade/bootstrap4/carousel.py b/cmsplugin_cascade/bootstrap4/carousel.py index a838d5e0a..e7c40b52c 100644 --- a/cmsplugin_cascade/bootstrap4/carousel.py +++ b/cmsplugin_cascade/bootstrap4/carousel.py @@ -5,20 +5,20 @@ from django.utils.safestring import mark_safe from django.utils.translation import ungettext_lazy, ugettext_lazy as _ -from entangled.forms import EntangledModelFormMixin +from entangled.forms import EntangledModelFormMixin, EntangledFormField, EntangledForm from cms.plugin_pool import plugin_pool from cmsplugin_cascade.bootstrap4.fields import BootstrapMultiSizeField from cmsplugin_cascade.bootstrap4.grid import Breakpoint from cmsplugin_cascade.bootstrap4.picture import get_picture_elements from cmsplugin_cascade.bootstrap4.plugin_base import BootstrapPluginBase from cmsplugin_cascade.bootstrap4.utils import IMAGE_RESIZE_OPTIONS -from cmsplugin_cascade.forms import ManageChildrenFormMixin +from cmsplugin_cascade.forms import ManageChildrenFormMixin, ManageNestedFormMixin from cmsplugin_cascade.image import ImagePropertyMixin, ImageFormMixin logger = logging.getLogger('cascade') -class CarouselSlidesFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): +class CarouselSlidesForm(EntangledForm): OPTION_CHOICES = [('slide', _("Animate")), ('pause', _("Pause")), ('wrap', _("Wrap"))] num_children = IntegerField(min_value=1, initial=1, @@ -55,9 +55,13 @@ class CarouselSlidesFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): initial=['upscale', 'crop', 'subject_location', 'high_resolution'], ) +class CarouselSlidesFormMixin(EntangledModelFormMixin, ManageNestedFormMixin): + carousel_nested = EntangledFormField(CarouselSlidesForm) + class Meta: - untangled_fields = ['num_children'] - entangled_fields = {'glossary': ['interval', 'options', 'container_max_heights', 'resize_options']} + #untangled_fields = ['num_children'] + # untangled_fields = ['accordion_nested.num_children'] + entangled_fields = {'glossary': ['carousel_nested']} class BootstrapCarouselPlugin(BootstrapPluginBase): @@ -79,7 +83,7 @@ def get_identifier(cls, obj): @classmethod def get_css_classes(cls, obj): css_classes = cls.super(BootstrapCarouselPlugin, cls).get_css_classes(obj) - if 'slide' in obj.glossary.get('options', []): + if 'slide' in obj.glossary.get('carousel_nested',{}).get('options', []): css_classes.append('slide') return css_classes @@ -87,14 +91,14 @@ def get_css_classes(cls, obj): def get_html_tag_attributes(cls, obj): attributes = cls.super(BootstrapCarouselPlugin, cls).get_html_tag_attributes(obj) attributes.update(cls.DEFAULT_CAROUSEL_ATTRIBUTES) - attributes['data-interval'] = 1000 * int(obj.glossary.get('interval', 5)) - options = obj.glossary.get('options', []) + attributes['data-interval'] = 1000 * int(obj.glossary.get('carousel_nested',{}).get('interval', 5)) + options = obj.glossary.get('carousel_nested',{}).get('options', []) attributes['data-pause'] = 'pause' in options and 'hover' or 'false' attributes['data-wrap'] = 'wrap' in options and 'true' or 'false' return attributes def save_model(self, request, obj, form, change): - wanted_children = int(form.cleaned_data.get('num_children')) + wanted_children = int(form.cleaned_data.get('carousel_nested',{}).get('num_children', 1)) super().save_model(request, obj, form, change) self.extend_children(obj, wanted_children, BootstrapCarouselSlidePlugin) obj.sanitize_children() @@ -104,11 +108,11 @@ def sanitize_model(cls, obj): sanitized = super().sanitize_model(obj) complete_glossary = obj.get_complete_glossary() # fill all invalid heights for this container to a meaningful value - max_height = max(obj.glossary['container_max_heights'].values()) + max_height = max(obj.glossary.get('carousel_nested',{})['container_max_heights'].values()) pattern = re.compile(r'^(\d+)px$') for bp in complete_glossary.get('breakpoints', ()): - if not pattern.match(obj.glossary['container_max_heights'].get(bp, '')): - obj.glossary['container_max_heights'][bp] = max_height + if not pattern.match(obj.glossary.get('carousel_nested',{})['container_max_heights'].get(bp, '')): + obj.glossary.get('carousel_nested',{})['container_max_heights'][bp] = max_height return sanitized plugin_pool.register_plugin(BootstrapCarouselPlugin) @@ -130,7 +134,7 @@ def render(self, context, instance, placeholder): # slide image shall be rendered in a responsive context using the ```` element try: parent_glossary = instance.parent.get_bound_plugin().glossary - instance.glossary.update(responsive_heights=parent_glossary['container_max_heights']) + instance.glossary.update(responsive_heights=parent_glossary.get('carousel_nested',{})['container_max_heights']) elements = get_picture_elements(instance) except Exception as exc: logger.warning("Unable generate picture elements. Reason: {}".format(exc)) @@ -144,27 +148,29 @@ def render(self, context, instance, placeholder): @classmethod def sanitize_model(cls, obj): sanitized = super().sanitize_model(obj) - resize_options = obj.get_parent_glossary().get('resize_options', []) + resize_options = obj.get_parent_glossary().get('carousel_nested',{}).get('resize_options', []) if obj.glossary.get('resize_options') != resize_options: obj.glossary.update(resize_options=resize_options) sanitized = True + parent = obj.parent - while parent.plugin_type != 'BootstrapColumnPlugin': - parent = parent.parent - if parent is None: - logger.warning("PicturePlugin(pk={}) has no ColumnPlugin as ancestor.".format(obj.pk)) - return - grid_column = parent.get_bound_plugin().get_grid_instance() - obj.glossary.setdefault('media_queries', {}) - for bp in Breakpoint: - obj.glossary['media_queries'].setdefault(bp.name, {}) - width = round(grid_column.get_bound(bp).max) - if obj.glossary['media_queries'][bp.name].get('width') != width: - obj.glossary['media_queries'][bp.name]['width'] = width - sanitized = True - if obj.glossary['media_queries'][bp.name].get('media') != bp.media_query: - obj.glossary['media_queries'][bp.name]['media'] = bp.media_query - sanitized = True + if parent: # prevent copy slide error + while parent.plugin_type != 'BootstrapColumnPlugin': + parent = parent.parent + if parent is None: + logger.warning("PicturePlugin(pk={}) has no ColumnPlugin as ancestor.".format(obj.pk)) + return + grid_column = parent.get_bound_plugin().get_grid_instance() + obj.glossary.setdefault('media_queries', {}) + for bp in Breakpoint: + obj.glossary['media_queries'].setdefault(bp.name, {}) + width = round(grid_column.get_bound(bp).max) + if obj.glossary['media_queries'][bp.name].get('width') != width: + obj.glossary['media_queries'][bp.name]['width'] = width + sanitized = True + if obj.glossary['media_queries'][bp.name].get('media') != bp.media_query: + obj.glossary['media_queries'][bp.name]['media'] = bp.media_query + sanitized = True return sanitized @classmethod diff --git a/cmsplugin_cascade/bootstrap4/container.py b/cmsplugin_cascade/bootstrap4/container.py index e7fdbc463..64e3d3f2b 100644 --- a/cmsplugin_cascade/bootstrap4/container.py +++ b/cmsplugin_cascade/bootstrap4/container.py @@ -7,10 +7,10 @@ from django.utils.text import format_lazy from django.utils.translation import ungettext_lazy, ugettext_lazy as _ from cms.plugin_pool import plugin_pool -from entangled.forms import EntangledModelFormMixin +from entangled.forms import EntangledModelFormMixin, EntangledFormField, EntangledForm from cmsplugin_cascade import app_settings from cmsplugin_cascade.bootstrap4.grid import Breakpoint -from cmsplugin_cascade.forms import ManageChildrenFormMixin +from cmsplugin_cascade.forms import ManageChildrenFormMixin, ManageNestedFormMixin from .plugin_base import BootstrapPluginBase from . import grid @@ -31,8 +31,7 @@ def get_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' - -class ContainerFormMixin(EntangledModelFormMixin): +class ContainerForm(EntangledForm): breakpoints = MultipleChoiceField( label=_('Available Breakpoints'), choices=get_widget_choices(), @@ -48,21 +47,28 @@ class ContainerFormMixin(EntangledModelFormMixin): help_text=_("Changing your outermost '.container' to '.container-fluid'.") ) - class Meta: - entangled_fields = {'glossary': ['breakpoints', 'fluid']} - def clean_breapoints(self): # TODO: check this - if len(self.cleaned_data['glossary']['breakpoints']) == 0: + if len(self.cleaned_data['glossary'].get('container_nested',{})['breakpoints']) == 0: raise ValidationError(_("At least one breakpoint must be selected.")) return self.cleaned_data['glossary'] +class ContainerFormMixin(EntangledModelFormMixin, ManageNestedFormMixin): + container_nested = EntangledFormField(ContainerForm) + + class Meta: + entangled_fields = {'glossary': ['container_nested']} + + + class ContainerGridMixin(object): def get_grid_instance(self): - fluid = self.glossary.get('fluid', False) + print('self.glossary') + print(self.glossary) + fluid = self.glossary.get('container_nested',{}).get('fluid', False) try: - breakpoints = [getattr(grid.Breakpoint, bp) for bp in self.glossary['breakpoints']] + breakpoints = [getattr(grid.Breakpoint, bp) for bp in self.glossary.get('container_nested',{})['breakpoints']] except KeyError: breakpoints = [bp for bp in grid.Breakpoint] if fluid: @@ -85,8 +91,8 @@ class BootstrapContainerPlugin(BootstrapPluginBase): @classmethod def get_identifier(cls, obj): - breakpoints = obj.glossary.get('breakpoints') - content = obj.glossary.get('fluid') and '(fluid) ' or '' + breakpoints = obj.glossary.get('container_nested',{}).get('breakpoints') + content = obj.glossary.get('container_nested',{}).get('fluid') and '(fluid) ' or '' if breakpoints: BREAKPOINTS = app_settings.CMSPLUGIN_CASCADE['bootstrap4']['fluid_bounds'] devices = ', '.join([str(bp.label) for bp in BREAKPOINTS if bp.name in breakpoints]) @@ -96,7 +102,7 @@ def get_identifier(cls, obj): @classmethod def get_css_classes(cls, obj): css_classes = cls.super(BootstrapContainerPlugin, cls).get_css_classes(obj) - if obj.glossary.get('fluid'): + if obj.glossary.get('container_nested',{}).get('fluid'): css_classes.append('container-fluid') else: css_classes.append('container') @@ -108,8 +114,7 @@ def save_model(self, request, obj, form, change): plugin_pool.register_plugin(BootstrapContainerPlugin) - -class BootstrapRowFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): +class BootstrapRowForm(EntangledForm): """ Form class to add non-materialized field to count the number of children. """ @@ -121,9 +126,16 @@ class BootstrapRowFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin): help_text=_('Number of columns to be created with this row.'), ) - class Meta: - untangled_fields = ['num_children'] +class BootstrapRowFormMixin(ManageChildrenFormMixin, EntangledModelFormMixin, ManageNestedFormMixin): + """ + Form class to add non-materialized field to count the number of children. + """ + row_nested = EntangledFormField(BootstrapRowForm) + + class Meta: + untangled_fields = ['row_nested.num_children'] + entangled_fields = {'glossary': ['row_nested']} class RowGridMixin(object): def get_grid_instance(self): @@ -149,7 +161,7 @@ def get_identifier(cls, obj): return mark_safe(content) def save_model(self, request, obj, form, change): - wanted_children = int(form.cleaned_data.get('num_children')) + wanted_children = int(form.cleaned_data.get('row_nested.num_children', 1)) super().save_model(request, obj, form, change) child_glossary = {'xs-column-width': 'col'} self.extend_children(obj, wanted_children, BootstrapColumnPlugin, child_glossary=child_glossary) @@ -188,7 +200,7 @@ class BootstrapColumnPlugin(BootstrapPluginBase): def get_form(self, request, obj=None, **kwargs): def choose_help_text(*phrases): - bounds = 'fluid_bounds' if container.glossary.get('fluid') else 'default_bounds' + bounds = 'fluid_bounds' if container.glossary.get('container_nested',{}).get('fluid') else 'default_bounds' bs4_breakpoints = app_settings.CMSPLUGIN_CASCADE['bootstrap4'][bounds] if last: return phrases[0].format(bs4_breakpoints[last].max) @@ -206,7 +218,8 @@ def choose_help_text(*phrases): else: jumbotrons=obj.get_ancestors().filter(plugin_type='BootstrapJumbotronPlugin') container=jumbotrons.order_by('depth').last().get_bound_plugin() - breakpoints = container.glossary['breakpoints'] + + breakpoints = container.glossary.get('container_nested',{}).get('breakpoints' , []) width_fields, offset_fields, reorder_fields, responsive_fields = {}, {}, {}, {} units = [ungettext_lazy("{} unit", "{} units", i).format(i) for i in range(0, 13)] @@ -329,6 +342,7 @@ class Meta: entangled_fields = {'glossary': glossary_fields} attrs = dict(width_fields, **offset_fields, **reorder_fields, **responsive_fields, Meta=Meta) + #ColumnForm = type('ColumnForm', (EntangledForm,), attrs) kwargs['form'] = type('ColumnForm', (EntangledModelFormMixin,), attrs) return super().get_form(request, obj, **kwargs) @@ -345,7 +359,7 @@ def sanitize_model(cls, obj): def get_identifier(cls, obj): glossary = obj.get_complete_glossary() widths = [] - for bp in glossary.get('breakpoints', []): + for bp in glossary.get('container_nested',{}).get('breakpoints', []): width = obj.glossary.get('{0}-column-width'.format(bp), '').replace('col-{0}-'.format(bp), '') if width: widths.append(width) diff --git a/cmsplugin_cascade/bootstrap4/embeds.py b/cmsplugin_cascade/bootstrap4/embeds.py index d97ed37a0..4956b59d9 100644 --- a/cmsplugin_cascade/bootstrap4/embeds.py +++ b/cmsplugin_cascade/bootstrap4/embeds.py @@ -4,7 +4,8 @@ from django.forms.fields import BooleanField, ChoiceField, URLField, Field from django.utils.six.moves.urllib.parse import urlparse, urlunparse, ParseResult from django.utils.translation import ugettext_lazy as _ -from entangled.forms import EntangledModelFormMixin, EntangledField +from entangled.forms import EntangledModelFormMixin, EntangledForm, EntangledFormField +from entangled.fields import EntangledInvisibleField from cms.plugin_pool import plugin_pool from cmsplugin_cascade.bootstrap4.plugin_base import BootstrapPluginBase @@ -17,7 +18,7 @@ class YoutubeFormMixin(EntangledModelFormMixin): ('embed-responsive-1by1', _("Responsive 1:1")), ] - videoid = EntangledField() + videoid = EntangledInvisibleField() url = URLField( label=_("YouTube URL"), diff --git a/cmsplugin_cascade/bootstrap4/icon.py b/cmsplugin_cascade/bootstrap4/icon.py index 0a47647ad..42f87fe67 100644 --- a/cmsplugin_cascade/bootstrap4/icon.py +++ b/cmsplugin_cascade/bootstrap4/icon.py @@ -8,9 +8,10 @@ from cmsplugin_cascade.link.plugin_base import LinkElementMixin from cmsplugin_cascade.icon.forms import IconFormMixin from cmsplugin_cascade.icon.plugin_base import IconPluginMixin +from entangled.forms import EntangledModelFormMixin, EntangledFormField, EntangledForm +from cmsplugin_cascade.forms import ManageNestedFormMixin - -class FramedIconFormMixin(IconFormMixin): +class FramedIconForm(EntangledForm): SIZE_CHOICES = [('{}em'.format(c), "{} em".format(c)) for c in range(1, 13)] RADIUS_CHOICES = [(None, _("Square"))] + \ @@ -56,9 +57,12 @@ class FramedIconFormMixin(IconFormMixin): required=False, ) + +class FramedIconFormMixin(IconFormMixin, ManageNestedFormMixin): + icon_nested = EntangledFormField(FramedIconForm) + class Meta: - entangled_fields = {'glossary': ['font_size', 'color', 'background_color', 'text_align', 'border', - 'border_radius']} + entangled_fields = {'glossary': ['icon_nested']} class FramedIconPlugin(IconPluginMixin, LinkPluginBase): @@ -76,13 +80,13 @@ class Media: @classmethod def get_tag_type(self, instance): - if instance.glossary.get('text_align') or instance.glossary.get('font_size'): + if instance.glossary.get('icon_nested',{}).get('text_align') or instance.glossary.get('icon_nested',{}).get('font_size'): return 'div' @classmethod def get_css_classes(cls, instance): css_classes = cls.super(FramedIconPlugin, cls).get_css_classes(instance) - text_align = instance.glossary.get('text_align') + text_align = instance.glossary.get('icon_nested',{}).get('text_align') if text_align: css_classes.append(text_align) return css_classes @@ -90,22 +94,22 @@ def get_css_classes(cls, instance): @classmethod def get_inline_styles(cls, instance): inline_styles = cls.super(FramedIconPlugin, cls).get_inline_styles(instance) - inline_styles['font-size'] = instance.glossary.get('font_size', '1em') + #inline_styles['font-size'] = instance.glossary.get('icon_nested',{}).get('font_size','1em') return inline_styles def render(self, context, instance, placeholder): context = self.super(FramedIconPlugin, self).render(context, instance, placeholder) styles = {'display': 'inline-block'} - color, inherit = instance.glossary.get('color', (ColorField.DEFAULT_COLOR, True)) + color, inherit = instance.glossary.get('icon_nested',{}).get('color', (ColorField.DEFAULT_COLOR, True)) if not inherit: styles['color'] = color - background_color, inherit = instance.glossary.get('background_color', (ColorField.DEFAULT_COLOR, True)) + background_color, inherit = instance.glossary.get('icon_nested',{}).get('background_color', (ColorField.DEFAULT_COLOR, True)) if not inherit: styles['background-color'] = background_color - border = instance.glossary.get('border') + border = instance.glossary.get('icon_nested',{}).get('border') if isinstance(border, list) and border[0] and border[1] != 'none': styles.update(border='{0} {1} {2}'.format(*border)) - radius = instance.glossary.get('border_radius') + radius = instance.get('icon_nested',{}).glossary.get('border_radius') if radius: styles['border-radius'] = radius attrs = [] diff --git a/cmsplugin_cascade/bootstrap4/image.py b/cmsplugin_cascade/bootstrap4/image.py index d3f536e95..e077de54b 100644 --- a/cmsplugin_cascade/bootstrap4/image.py +++ b/cmsplugin_cascade/bootstrap4/image.py @@ -10,11 +10,13 @@ from cmsplugin_cascade.fields import SizeField from cmsplugin_cascade.link.config import LinkPluginBase, LinkFormMixin from cmsplugin_cascade.link.plugin_base import LinkElementMixin +from entangled.forms import EntangledModelFormMixin, EntangledFormField, EntangledForm +from cmsplugin_cascade.forms import ManageNestedFormMixin logger = logging.getLogger('cascade.bootstrap4') -class BootstrapImageFormMixin(ImageFormMixin): +class BootstrapImageForm(EntangledForm): ALIGNMENT_OPTIONS = [ ('float-left', _("Left")), ('float-right', _("Right")), @@ -67,9 +69,12 @@ class BootstrapImageFormMixin(ImageFormMixin): help_text=_("How to align a non-responsive image."), ) + +class BootstrapImageFormMixin(ImageFormMixin): + images_nested = EntangledFormField(BootstrapImageForm) + class Meta: - entangled_fields = {'glossary': ['image_shapes', 'image_width_responsive', 'image_width_fixed', - 'image_height', 'resize_options', 'image_alignment']} + entangled_fields = {'glossary': ['images_nested']} class BootstrapImagePlugin(LinkPluginBase): diff --git a/cmsplugin_cascade/forms.py b/cmsplugin_cascade/forms.py index 5681dac69..ac241146d 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_nested = field_name.split('.') + 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/icon/plugin_base.py b/cmsplugin_cascade/icon/plugin_base.py index 23b6e58aa..20eece6c6 100644 --- a/cmsplugin_cascade/icon/plugin_base.py +++ b/cmsplugin_cascade/icon/plugin_base.py @@ -1,7 +1,7 @@ from django.utils.safestring import mark_safe from cmsplugin_cascade.models import IconFont from cmsplugin_cascade.plugin_base import CascadePluginMixinBase -from entangled.forms import get_related_object +from entangled.utils import get_related_object class IconPluginMixin(CascadePluginMixinBase): diff --git a/cmsplugin_cascade/image.py b/cmsplugin_cascade/image.py index 6142511d8..d8e4071e7 100644 --- a/cmsplugin_cascade/image.py +++ b/cmsplugin_cascade/image.py @@ -1,8 +1,10 @@ from django.core.exceptions import ValidationError -from django.forms.fields import CharField +from django.forms.fields import Field, CharField from django.utils.translation import ugettext_lazy as _ -from entangled.forms import EntangledModelFormMixin, EntangledField, get_related_object +from entangled.forms import EntangledModelFormMixin, EntangledFormField , EntangledModelForm +from entangled.utils import get_related_object from cmsplugin_cascade.fields import CascadeImageField +from entangled.fields import EntangledInvisibleField class ImageFormMixin(EntangledModelFormMixin): @@ -20,7 +22,7 @@ class ImageFormMixin(EntangledModelFormMixin): help_text=_("Textual description of the image added to the 'alt' tag of the element."), ) - _image_properties = EntangledField() + _image_properties = EntangledInvisibleField() class Meta: entangled_fields = {'glossary': ['image_file', 'image_title', 'alt_tag', '_image_properties']} diff --git a/cmsplugin_cascade/link/forms.py b/cmsplugin_cascade/link/forms.py index dbd56328d..0e0ade5d1 100644 --- a/cmsplugin_cascade/link/forms.py +++ b/cmsplugin_cascade/link/forms.py @@ -12,7 +12,8 @@ from cms.utils import get_current_site from cms.models import Page -from entangled.forms import EntangledModelFormMixin, get_related_object +from entangled.forms import EntangledModelFormMixin +from entangled.utils import get_related_object from filer.models.filemodels import File as FilerFileModel from filer.fields.file import AdminFileWidget, FilerFileField diff --git a/cmsplugin_cascade/link/plugin_base.py b/cmsplugin_cascade/link/plugin_base.py index bb8739bc0..5bbb4f619 100644 --- a/cmsplugin_cascade/link/plugin_base.py +++ b/cmsplugin_cascade/link/plugin_base.py @@ -1,7 +1,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils.functional import cached_property from django.utils.safestring import mark_safe -from entangled.forms import get_related_object +from entangled.utils import get_related_object from cmsplugin_cascade.plugin_base import CascadePluginBase from filer.models.filemodels import File as FilerFileModel diff --git a/cmsplugin_cascade/plugin_base.py b/cmsplugin_cascade/plugin_base.py index 0150758bd..6ba7ff1f8 100644 --- a/cmsplugin_cascade/plugin_base.py +++ b/cmsplugin_cascade/plugin_base.py @@ -17,7 +17,7 @@ from .extra_fields.mixins import ExtraFieldsMixin from .hide_plugins import HidePluginMixin from .render_template import RenderTemplateMixin -from .utils import remove_duplicates +from .utils import remove_duplicates, gen_fieldsets_by_entangled_form mark_safe_lazy = lazy(mark_safe, str) @@ -324,7 +324,11 @@ 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, { }) + kwargs['fields'] = form.declared_fields + kwargs['form'] = form + self.fieldsets = gen_fieldsets_by_entangled_form(form) return super().get_form(request, obj, **kwargs) def get_parent_instance(self, request=None, obj=None): diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html b/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html index ae3d899c4..dfb8f3bad 100644 --- a/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/accordion.html @@ -1,5 +1,4 @@ {% load l10n cascade_tags %} - {% localize off %}{% spaceless %}{% with inline_styles=instance.inline_styles plugin_id=instance.id %}
{% for card in instance.child_plugin_instances %} @@ -7,7 +6,7 @@
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/carousel-slide.html b/cmsplugin_cascade/templates/cascade/bootstrap4/carousel-slide.html index 1cf6f6864..047413fa2 100644 --- a/cmsplugin_cascade/templates/cascade/bootstrap4/carousel-slide.html +++ b/cmsplugin_cascade/templates/cascade/bootstrap4/carousel-slide.html @@ -4,4 +4,4 @@ -{% endif %} \ No newline at end of file +{% endif %} diff --git a/cmsplugin_cascade/utils.py b/cmsplugin_cascade/utils.py index 3142da131..ecb39d068 100644 --- a/cmsplugin_cascade/utils.py +++ b/cmsplugin_cascade/utils.py @@ -81,6 +81,25 @@ def parse_responsive_length(responsive_length): return (None, float(responsive_length.rstrip('%')) / 100) return (None, None) +def gen_fieldsets_by_entangled_form(form): + """ + Generate fieldsets with group_by if find nested entangled form. + """ + fieldsets = () + group_by = {} + for key_field , field in form.base_fields.items(): + if len(key_field.split('.')) == 2 : + nested_group, field_name = key_field.split('.') + group_by.setdefault(nested_group, []) + group_by[nested_group].append( key_field ) + else : + if form.declared_fields[ key_field ]: + group_by.setdefault('fields_root', []) + group_by['fields_root'].append(key_field ) + for group, list_fields in group_by.items(): + fieldsets +=(None, {'fields':list_fields }), + return fieldsets + class CascadeUtilitiesMixin(metaclass=MediaDefiningClass): """ 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/pyproject.toml b/examples/pyproject.toml index e805b01a8..d24e7dce5 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -11,6 +11,8 @@ 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-entangled = { git = "https://github.com/haricot/django-entangled.git", branch="nested_modif" } 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..ae46cb7b6 100644 --- a/tests/bootstrap4/test_accordion.py +++ b/tests/bootstrap4/test_accordion.py @@ -19,13 +19,13 @@ def bootstrap_accordion(rf, admin_site, bootstrap_column): assert isinstance(accordion_model, CascadeElement) accordion_plugin = accordion_model.get_plugin_class_instance(admin_site) assert isinstance(accordion_plugin, BootstrapAccordionPlugin) - data = {'num_children': 2, 'close_others': 'on', 'first_is_open': 'on'} + data = {'accordion_nested.num_children': 2, 'accordion_nested.close_others': 'on', 'accordion_nested.first_is_open': 'on'} ModelForm = accordion_plugin.get_form(request, accordion_model) form = ModelForm(data, None, instance=accordion_model) assert form.is_valid() accordion_plugin.save_model(request, accordion_model, form, False) - assert accordion_model.glossary['close_others'] is True - assert accordion_model.glossary['first_is_open'] is True + assert accordion_model.glossary['accordion_nested']['close_others'] is True + assert accordion_model.glossary['accordion_nested']['first_is_open'] is True for child in accordion_model.get_children(): assert isinstance(child.get_plugin_class_instance(admin_site), BootstrapAccordionGroupPlugin) return accordion_plugin, accordion_model @@ -37,13 +37,13 @@ def test_edit_accordion_group(rf, admin_site, bootstrap_accordion): accordion_plugin, accordion_model = bootstrap_accordion first_group = accordion_model.get_first_child() group_model, group_plugin = first_group.get_plugin_instance(admin_site) - data = {'heading': "Hello", 'body_padding': 'on'} + data = {'accordion_nested.heading': "Hello", 'accordion_nested.body_padding': 'on'} ModelForm = group_plugin.get_form(request, group_model) form = ModelForm(data, None, instance=group_model) assert form.is_valid() group_plugin.save_model(request, group_model, form, False) - assert group_model.glossary['heading'] == "Hello" - assert group_model.glossary['body_padding'] is True + assert group_model.glossary['accordion_nested']['heading'] == "Hello" + assert group_model.glossary['accordion_nested']['body_padding'] is True # render the plugin build_plugin_tree([accordion_model, group_model]) diff --git a/tests/bootstrap4/test_container.py b/tests/bootstrap4/test_container.py index cd4b5715d..302d2d4fc 100644 --- a/tests/bootstrap4/test_container.py +++ b/tests/bootstrap4/test_container.py @@ -5,26 +5,26 @@ from cms.utils.plugins import build_plugin_tree from cmsplugin_cascade.models import CascadeElement from cmsplugin_cascade.bootstrap4.container import BootstrapColumnPlugin - +from entangled.forms import EntangledModelFormMixin, EntangledFormField, EntangledForm @pytest.mark.django_db def test_edit_bootstrap_container(rf, bootstrap_container): container_plugin, container_model = bootstrap_container request = rf.get('/') ModelForm = container_plugin.get_form(request, container_model) - data = {'breakpoints': ['sm', 'md']} + data = {'container_nested.breakpoints': ['sm', 'md']} form = ModelForm(data, None, instance=container_model) assert form.is_valid() soup = BeautifulSoup(form.as_p(), features='lxml') - input_element = soup.find(id="id_breakpoints_0") - assert {'type': 'checkbox', 'name': 'breakpoints', 'value': 'xs'}.items() <= input_element.attrs.items() - input_element = soup.find(id="id_breakpoints_2") - assert {'type': 'checkbox', 'name': 'breakpoints', 'value': 'md', 'checked': ''}.items() <= input_element.attrs.items() - input_element = soup.find(id="id_fluid") - assert {'type': 'checkbox', 'name': 'fluid'}.items() <= input_element.attrs.items() + input_element = soup.find(id="id_container_nested.breakpoints_0") + assert {'type': 'checkbox', 'name': 'container_nested.breakpoints', 'value': 'xs'}.items() <= input_element.attrs.items() + input_element = soup.find(id="id_container_nested.breakpoints_2") + assert {'type': 'checkbox', 'name': 'container_nested.breakpoints', 'value': 'md', 'checked': ''}.items() <= input_element.attrs.items() + input_element = soup.find(id="id_container_nested.fluid") + assert {'type': 'checkbox', 'name': 'container_nested.fluid'}.items() <= input_element.attrs.items() container_plugin.save_model(request, container_model, form, False) - assert container_model.glossary['breakpoints'] == ['sm', 'md'] - assert 'fluid' in container_model.glossary + assert container_model.glossary['container_nested']['breakpoints'] == ['sm', 'md'] + assert 'fluid' in container_model.glossary['container_nested'] assert str(container_model) == "for Landscape Phones, Tablets" @@ -33,7 +33,7 @@ def test_edit_bootstrap_row(rf, bootstrap_row): row_plugin, row_model = bootstrap_row request = rf.get('/') ModelForm = row_plugin.get_form(request, row_model) - data = {'num_children': 3} + data = {'row_nested.num_children': 3} form = ModelForm(data, None, instance=row_model) assert form.is_valid() row_plugin.save_model(request, row_model, form, False) diff --git a/tests/requirements.txt b/tests/requirements.txt index 9f03e2295..1fce4b73a 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,5 @@ -lxml==3.5.0 +lxml +#lxml==3.5.0 pluggy==0.12.0 py==1.8.0 pytest==4.5.0 @@ -8,3 +9,5 @@ virtualenv==13.1.2 django-reversion==3.0.3 factory-boy==2.12.0 pytest-factoryboy==2.0.3 +#https://github.com/jrief/django-entangled/archive/develop.zip +https://github.com/haricot/django-entangled/archive/nested_modif.zip diff --git a/tests/static/strides.json b/tests/static/strides.json index 07876595a..e8add5e4f 100644 --- a/tests/static/strides.json +++ b/tests/static/strides.json @@ -398,7 +398,7 @@ "FramedIconPlugin", { "pk":1596, - "glossary":{ + "glossary":{ "icon_nested":{ "border_radius":"", "text_align":"text-center", "icon_font":"1", @@ -418,7 +418,7 @@ ], "hide_plugin":"", "symbol":"umbrella" - } + }} }, [] ], @@ -1125,4 +1125,4 @@ ] ] ] -} \ No newline at end of file +}