Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extra_html_tag_attribute feature #318

Closed
wants to merge 15 commits into from
9 changes: 8 additions & 1 deletion cmsplugin_cascade/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ 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,
BorderChoiceField)
BorderChoiceField,HmltAttrsWidget)

if hasattr(self, '_config_CMSPLUGIN_CASCADE'):
return self._config_CMSPLUGIN_CASCADE
Expand Down Expand Up @@ -107,6 +108,12 @@ def CMSPLUGIN_CASCADE(self):
'Overflow',
(['overflow', 'overflow-x', 'overflow-y'], SelectOverflowField))

config.setdefault('extra_html_tag_attributes', OrderedDict())
extra_html_tag_attributes = config['extra_html_tag_attributes']
extra_html_tag_attributes.setdefault(
'HtmlAttrs',
(('Multiples_attrs',), HmltAttrsWidget ))

if 'cmsplugin_cascade.segmentation' in INSTALLED_APPS:
config.setdefault('segmentation_mixins', [
('cmsplugin_cascade.segmentation.mixins.EmulateUserModelMixin',
Expand Down
3 changes: 2 additions & 1 deletion cmsplugin_cascade/extra_fields/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ class PluginExtraFieldsConfig(object):
:param allow_override:
If ``True``, allows to override this configuration using the admin's backend interface.
"""
def __init__(self, allow_id_tag=False, css_classes=None, inline_styles=None, allow_override=True):
def __init__(self, allow_id_tag=False, css_classes=None, inline_styles=None, html_tag_attributes=None, allow_override=True):
self.allow_id_tag = allow_id_tag
self.css_classes = dict(multiple='', class_names='') if css_classes is None else dict(css_classes)
self.inline_styles = {} if inline_styles is None else dict(inline_styles)
self.html_tag_attributes = {} if html_tag_attributes is None else dict(html_tag_attributes)
self.allow_override = allow_override
35 changes: 34 additions & 1 deletion cmsplugin_cascade/extra_fields/mixins.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ObjectDoesNotExist
from distutils.version import LooseVersion
from django.forms import MediaDefiningClass, widgets
from django.forms.fields import CharField, ChoiceField, MultipleChoiceField
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from urllib.parse import quote, urlparse
from cms.utils.page import get_page_from_request

from entangled.forms import EntangledModelFormMixin

from cmsplugin_cascade import app_settings
from cmsplugin_cascade.fields import SizeField

Expand Down Expand Up @@ -79,6 +84,23 @@ def get_form(self, request, obj=None, **kwargs):
field_kwargs['allowed_units'] = extra_fields.inline_styles.get('extra_units:{0}'.format(style)).split(',')
form_fields[key] = Field(**field_kwargs)

# add input fields to let the user enter html tag attibutes information
for html_tag_attrs, choices_tuples in app_settings.CMSPLUGIN_CASCADE['extra_html_tag_attributes'].items():
html_tag_attributes = extra_fields.html_tag_attributes.get('extra_fields:{0}'.format(html_tag_attrs))
if html_tag_attributes is not None:
for data_set in html_tag_attributes:
Widget = choices_tuples[1]
if isinstance(data_set, tuple):
Widget.widget_name = data_set[0]
Widget.attributes_extra = data_set[1]
if 'widget_choices_cms_page' in str(Widget.attributes_extra.values()):
cms_path = urlparse(request.GET.dict()['cms_path']).path
page = get_page_from_request(request, use_path=cms_path, clean_path=True)
Widget.current_page = page
key = 'extra_html_tag_attributes:{0}'.format(Widget.widget_name)
label = '{0}: {1}'.format( html_tag_attrs , Widget.widget_name)
glossary_fields.append(GlossaryField(Widget(), label=label, name=key))

# extend the form with some extra fields
base_form = kwargs.pop('form', self.form)
assert issubclass(base_form, EntangledModelFormMixin), "Form must inherit from EntangledModelFormMixin"
Expand Down Expand Up @@ -125,6 +147,17 @@ def get_inline_styles(cls, obj):
def get_html_tag_attributes(cls, obj):
attributes = super().get_html_tag_attributes(obj)
extra_element_id = obj.glossary.get('extra_element_id')
extra_html_tag_attributes = obj.glossary.get('extra_html_tag_attributes')
for key, eis in obj.glossary.items():
if key.startswith('extra_html_tag_attributes:'):
if isinstance(eis, dict):
attributes.update(dict((k, v) for k, v in eis.items() if v))
if isinstance(eis, (list, tuple)):
attributes.update({key.split(':')[1]: eis[0]})
attributes.update({key.split(':')[1]: eis[0]})
elif isinstance(eis, six.string_types):
attributes.update({key.split(':')[1]: eis})
attributes.update({key.split(':')[0]: eis})
if extra_element_id:
attributes.update(id=extra_element_id)
return attributes
Expand All @@ -135,4 +168,4 @@ def get_identifier(cls, obj):
extra_element_id = obj.glossary and obj.glossary.get('extra_element_id')
if extra_element_id:
return format_html('{0}<em>{1}:</em> ', identifier, extra_element_id)
return identifier
return identifier
126 changes: 124 additions & 2 deletions cmsplugin_cascade/widgets.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import re
import json
from html.parser import HTMLParser # py3

import warnings

from django.core.exceptions import ValidationError
from django.contrib.staticfiles.finders import find
from django.forms import Media, widgets
from django.utils.html import escape, format_html, format_html_join
from six.moves.html_parser import HTMLParser
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ugettext
from urllib.parse import quote, urlparse

from cmsplugin_cascade import app_settings
from cmsplugin_cascade.fields import GlossaryField
from cms.models import Page


class JSONMultiWidget(widgets.MultiWidget):
Expand Down Expand Up @@ -257,3 +264,118 @@ def get_context(self, name, value, attrs):
values[3] = self.rgb2hex(values[2])
context = super().get_context(name, values, attrs)
return context

class HmltAttrsWidget(widgets.MultiWidget):
"""
Use this field to enter a html attributes value.
The value passed to the GlossaryField is guaranteed to be validate text, int and color format.
Class HmltAttrsWidget has pre-set widget which can be entered in the settings file:
widget_choices_cms_page_anchors, widget_choices_list, widget_choices_int, widget_choices_text,
widget_choices_color, widget_attrs.
This class is made to work in conjunction with the Class ExtraFieldsMixin preferably.
"""
DEFAULT_ATTRS = {'style': 'padding-left: 26px;'}
invalid_message = _("In '%(label)s': Value '%(value)s' is not a valid.")
validation_pattern = re.compile(r'^[A-Za-z0-9_-]+$')
int_validation_pattern = re.compile(r'^[-+]?[0-9]+$')
color_validation_pattern = re.compile('^#[0-9a-f]{3}([0-9a-f]{3})?$')
all_extra_html_tag_attributes = {}

def __init__(self, attrs=DEFAULT_ATTRS):
attrs = dict(attrs)
self.list_attrs_name = self.attributes_extra.keys() if hasattr(self, 'attributes_extra') else None
widget_list = []
for key, val in self.attributes_extra.items():
self.all_extra_html_tag_attributes[self.widget_name] = self.attributes_extra
choices = []
if 'widget_attrs' in val:
widgetattrs = val['widget_attrs']
widgetattrs.update(attrs)
if 'widget_choices_list' in val:
attrs_add = {'style': 'width: 10em;'}
widgetattrs.update(attrs_add)
choices = val['widget_choices_list']
widget_list.append(widgets.Select(attrs=widgetattrs, choices=[(s, s) for s in choices]))
elif 'widget_choices_cms_page_anchors' in val :
attrs_add = {'style': 'width: 10em;'}
widgetattrs.update(attrs_add)
if hasattr(Page.objects.get(pk=self.current_page.pk), 'cascadepage'):
cascade_page = Page.objects.get(pk=self.current_page.pk).cascadepage
for cascade_id, cascade_id_value in cascade_page.glossary.get('element_ids', {}).items():
cascade_id_value='#{}'.format(cascade_id_value)
choices = [('inherit',_("Inherit")) , (cascade_id, cascade_id_value)]
else:
choices = [('inherit',_("Inherit"))]
widget_list.append(widgets.Select(attrs=widgetattrs, choices=[s for s in choices]))
elif 'widget_choices_int' in val:
attrs_add = {'style': 'width: 9em;', 'type': 'text'}
widgetattrs.update(attrs_add)
widget_list.append(widgets.TextInput(attrs=widgetattrs))
elif 'widget_choices_text' in val:
attrs_add = {'style': 'width: 9em;', 'type': 'text'}
widgetattrs.update(attrs_add)
widget_list.append(widgets.TextInput(attrs=widgetattrs))
elif 'widget_choices_color' in val:
attrs_add = {'style': 'width: 10em;', 'type': 'color'}
widgetattrs.update(attrs_add)
widget_list.append(widgets.TextInput(attrs=widgetattrs))
super(HmltAttrsWidget, self).__init__(widget_list)

def __iter__(self):
for name in self.list_attrs_name:
yield name

def decompress(self, values):
if not isinstance(values, (list, tuple, dict)):
values = {}
for attr_name in self.list_attrs_name:
values[attr_name] = ''
return values

def value_from_datadict(self, data, files, name):
self.widgetname = name.split(':')[-1]
values = {}
for attr_name in self.list_attrs_name:
data_attr = escape(data.get('{0}_{1}'.format(name, attr_name),''))
if data_attr == 'inherit' or data_attr == 'None':
data_attr = ""
values[attr_name] = data_attr
return values

def render(self, name, values, attrs):
values = values
attrs = dict(attrs)
html = '<div class="clearfix">'
html += '<div style="position: relative;">'
if isinstance(values, dict):
for index, attr_data_set in enumerate(values.items(), start=0):
attribute_name = attr_data_set[0]
attribute_values = attr_data_set[1]
subkey = '{0}_{1}'.format(name, attribute_name)
widget_attrs=[]
if 'widget_attrs' in attribute_values:
widget_attrs = attribute_values['widget_attrs']
html += format_html('<div style="float: left; width: 12em;" ><label for="{0}"><p>{1}</p>{2}</label></div>',
subkey, attribute_name , self.widgets[index].render(subkey, attribute_values, attrs ), widget_attrs)
html += '</div></div>'
return mark_safe(html)

def validate(self, values, key):
for index, v in enumerate(values.items(), start=0):
key_attibute_name = v[0]
value_attibute_value = v[1]
if value_attibute_value != '':
self.widgets[index].validate=True
values_extra_attr = self.all_extra_html_tag_attributes.get(self.widgetname, '').get( key_attibute_name, '')
if values_extra_attr:
if 'widget_choices_int' in values_extra_attr:
if not self.int_validation_pattern.match(value_attibute_value):
self.widgets[index].validate( value_attibute_value)
raise ValidationError(self.invalid_message, code='invalid', params={key_attibute_name : value_attibute_value})
elif 'widget_choices_color' in values_extra_attr:
if not self.color_validation_pattern.match(value_attibute_value):
self.widgets[index].validate(value_attibute_value)
raise ValidationError(self.invalid_message, code='invalid', params={key_attibute_name : value_attibute_value})
elif not self.validation_pattern.match(value_attibute_value):
self.widgets[index].validate(value_attibute_value)
raise ValidationError(self.invalid_message, code='invalid', params={key_attibute_name : value_attibute_value})
65 changes: 64 additions & 1 deletion examples/bs4demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import os
import sys

from django.urls import reverse_lazy

from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from cmsplugin_cascade.extra_fields.config import PluginExtraFieldsConfig
from django.utils.text import format_lazy

Expand Down Expand Up @@ -223,6 +224,68 @@
'allow_plugin_hiding': True,
'leaflet': {'default_position': {'lat': 50.0, 'lng': 12.0, 'zoom': 6}},
'cache_strides': True,
'plugins_with_extra_fields': {
'SimpleWrapperPlugin': PluginExtraFieldsConfig(
inline_styles={
'extra_fields:Colors': ['color', 'background-color'],
'extra_fields:Margins': ['margin-top', 'margin-right', 'margin-botton,', 'margin-left'],
'extra_units:Margins': 'px,em',
},
html_tag_attributes={
'extra_fields:HtmlAttrs': [
('Animate-on-scroll', {
'data-aos': {
'widget_choices_list': ["inherit", "fade", "fade-up", "fade-down", "fade-left", "fade-right", "fade-up-right", "fade-up-left", "fade-down-right", "fade-down-left", "flip-up", "flip-down", "flip-left", "flip-right", "slide-up", "slide-down", "slide-left", "slide-right", "zoom-in", "zoom-in-up", "zoom-in-down", "zoom-in-left", "zoom-in-right", "zoom-out", "zoom-out-up", "zoom-out-down", "zoom-out-left", "zoom-out-right"],
'widget_attrs': {'title': 'Animation name'},
},
'data-aos-offset': {
'widget_choices_list': ["inherit"] + [i for i in range(-500, 510, 10)],
'widget_attrs': {'title': 'Optional: Change offset to trigger animations sooner or later (px)'},
},
'data-aos-duration': {
'widget_choices_list': ["inherit"] + [i for i in range(0, 2100, 200)],
'widget_attrs': {'title': 'Optional: Duration of animation (ms)', 'style': '', },
},
'data-aos-easing': {
'widget_choices_list': ["inherit", "linear", "ease", "ease-in", "ease-out", "ease-in-out", "ease-in-back", "ease-out-back", "ease-in-out-back", "ease-in-sine", "ease-out-sine", "ease-in-out-sine", "ease-in-quad", "ease-out-quad", "ease-in-out-quad", "ease-in-cubic", "ease-out-cubic", "ease-in-out-cubic", "ease-in-quart", "ease-out-quart", "ease-in-out-quart"],
'widget_attrs': {'title': 'Optional: Choose timing function to ease elements in different ways'}
},
'data-aos-delay': {
'widget_choices_list': ["inherit"] + [i for i in range(0, 2100, 200)],
'widget_attrs': {'title': 'Optional: Delay animation (ms)'}
},
'data-aos-anchor': {
'widget_choices_cms_page_anchors': [],
'widget_attrs': {'title': 'Optional: Anchor element, whose offset will be counted to trigger animation instead of actual elements offset', },
},
'data-aos-anchor-placement': {
'widget_choices_list': ["inherit", "top-bottom", "top-center", "top-top", "center-bottom", "center-center", "center-top", "bottom-bottom", "bottom-center", "bottom-top"],
'widget_attrs': {'title': 'Optional: Anchor placement - which one position of element on the screen should trigger animation'},
},
'data-aos-once': {
'widget_choices_list': ['inherit', 'true', 'false'],
'widget_attrs': {'title': 'Optional: Choose wheter animation should fire once, or every time you scroll up/down to element'},
},
}
),
('Bootstrap-Scrollspy', {
'data-spy': {
'widget_choices_list': ["inherit", "scroll"],
'widget_attrs': {'title': 'Automatically update Bootstrap navigation or list group components based on scroll position to indicate which link is currently active in the viewport.', 'style': '', },
},
'data-target': {
'widget_choices_cms_page_anchors': [],
'widget_attrs': {'title': 'Anchors', 'style': '', },
},
'data-offset': {
'widget_choices_list': ["inherit"] + [i for i in range(-500, 510, 10)],
'widget_attrs': {'title': 'offset', 'style': '', },
},
}),
],
},
),
}
}

CMS_PLACEHOLDER_CONF = {
Expand Down