def test_validate_key(self): field = AttributesField() # Normal, expected patterns try: field.validate_key('target') field.validate_key('a') field.validate_key('A') field.validate_key('a1') field.validate_key('A1') field.validate_key('a-1') field.validate_key('a_1') field.validate_key('a-A1_') except ValidationError: self.fail('Keys that pass have failed.') # We don't accept these though... with self.assertRaises(ValidationError): field.validate_key('-abc') with self.assertRaises(ValidationError): field.validate_key('_abc') with self.assertRaises(ValidationError): field.validate_key('31-flavors') with self.assertRaises(ValidationError): field.validate_key('__init__') with self.assertRaises(ValidationError): field.validate_key('cöordinate') with self.assertRaises(ValidationError): field.validate_key('<tag>') with self.assertRaises(ValidationError): field.validate_key('#hash')
class Bootstrap3AccordionItemPlugin(CMSPlugin): """ JavaScript - Collapse: "Accordion item" Model http://getbootstrap.com/javascript/#collapse """ title = model_fields.MiniText( verbose_name=_('Title'), default='', blank=True, ) context = model_fields.Context( verbose_name=_('Context'), choices=(('default', _('Default')), ) + constants.CONTEXT_CHOICES, default='default', blank=False, ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def __str__(self): return self.title
class AbstractIcon(CMSPlugin): icon = Icon() template = models.CharField( verbose_name=_('Template'), choices=get_templates(), default=get_templates()[0][0], max_length=255, ) label = models.CharField( verbose_name=_('Label'), blank=True, max_length=255, ) attributes = AttributesField( verbose_name=_('Attributes'), blank=True, ) class Meta: abstract = True def __str__(self): return str(self.pk) def get_short_description(self): return self.label or ''
class Boostrap3JumbotronPlugin(CMSPlugin): """ Component - Jumbotron: Model http://getbootstrap.com/components/#jumbotron """ label = models.CharField( verbose_name=_('Label'), blank=True, max_length=255, ) grid = models.BooleanField( verbose_name=_('Add container'), default=False, blank=True, help_text=_('Adds a ".container" element inside the "Jumbotron" ' 'for use outside of a grid.'), ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def __str__(self): return self.label or str(self.pk)
class Boostrap3BlockquotePlugin(CMSPlugin): """ CSS - Typography: "Blockquote" Model http://getbootstrap.com/css/#type-blockquotes """ reverse = models.BooleanField( verbose_name=_('Reverse quote'), default=False, blank=True, help_text=_('Reverses the position by adding the Bootstrap 3 ' '"blockquote-reverse" class.'), ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def __str__(self): if self.reverse: return '.blockquote-reverse' return ''
class LinkPlugin(CMSPlugin): url_grouper = models.ForeignKey( UrlGrouper, on_delete=models.CASCADE, related_name='%(app_label)s_%(class)s_url_grouper', null=True) label = models.CharField(verbose_name=_("label"), max_length=120) template = models.CharField( verbose_name=_("Template"), choices=get_templates(), default=TEMPLATE_DEFAULT, max_length=255, ) target = models.CharField(verbose_name=_("Target"), choices=TARGET_CHOICES, blank=True, max_length=255) attributes = AttributesField(verbose_name=_("Attributes"), blank=True, excluded_keys=["href", "target"]) class Meta: verbose_name = _("url plugin model") verbose_name_plural = _("url plugin models") def __str__(self): return self.label
class NavLinkPluginConfig(PluginAbstractLink): text_class = models.CharField(max_length=50, choices=TEXT_CLASSES, blank=True, null=True) background_class = models.CharField(max_length=50, choices=BACKGROUND_CLASSES, blank=True, null=True) dropdown = models.BooleanField(default=False, null=False) attributes = AttributesField() @property def class_str(self): class_list = ["nav-link"] if self.dropdown: class_list.append("dropdown-toggle") if self.text_class: class_list.append(self.text_class) if self.background_class: class_list.append(self.background_class) return " ".join(class_list) def __str__(self): retn = "Nav Link: %s" % self.get_link if self.title: retn = "Nav Link: %s" % self.title return retn
class Bootstrap3RowPlugin(CMSPlugin): """ CSS - Grid system: "Row" Model http://getbootstrap.com/css/#grid """ classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def __str__(self): return str(self.pk) def get_short_description(self): instance = self.get_plugin_instance()[0] if not instance: return ugettext('<empty>') column_count = len(self.child_plugin_instances or []) column_count_str = ungettext('1 column', '%(count)i columns', column_count) % { 'count': column_count } if self.classes: return '{} ({})'.format(self.classes, column_count_str) return column_count_str
class LinkPlugin(CMSPlugin): internal_name = models.CharField(verbose_name=_("internal name"), max_length=120) url = models.ForeignKey(Url, verbose_name=_("url"), related_name="cms_plugins", on_delete=models.CASCADE) label = models.CharField(verbose_name=_("label"), max_length=120) template = models.CharField( verbose_name=_("Template"), choices=get_templates(), default=TEMPLATE_DEFAULT, max_length=255, ) target = models.CharField(verbose_name=_("Target"), choices=TARGET_CHOICES, blank=True, max_length=255) attributes = AttributesField(verbose_name=_("Attributes"), blank=True, excluded_keys=["href", "target"]) class Meta: verbose_name = _("url plugin model") verbose_name_plural = _("url plugin models") def __str__(self): return self.label
class NavBarBrandPluginConfig(PluginAbstractLink): attributes = AttributesField() def __str__(self): retn = "Navbar Brand: %s" % self.get_link return retn
class Bootstrap3AccordionPlugin(CMSPlugin): """ JavaScript - Collapse: "Accordion" Model http://getbootstrap.com/javascript/#collapse """ index = models.PositiveIntegerField( verbose_name=_('Index'), null=True, blank=True, help_text=_('Index of element to open on page load (optional).'), ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def get_short_description(self): instance = self.get_plugin_instance()[0] if not instance: return ugettext('<empty>') column_count = len(self.child_plugin_instances or []) column_count_str = ungettext('1 item', '%(count)i items', column_count) % { 'count': column_count } return column_count_str def __str__(self): return str(self.index)
class Bootstrap3CodePlugin(CMSPlugin): """ CSS - Code: Model http://getbootstrap.com/css/#code """ CODE_TYPE_CHOICES = ( ('code', _('Inline')), ('kbd', _('User input')), ('pre', _('Basic block')), ('var', _('Variables')), ('samp', _('Sample output')), ) code_type = models.CharField( verbose_name=_('Code type'), choices=CODE_TYPE_CHOICES, default=CODE_TYPE_CHOICES[0][0], max_length=255, ) code = models.TextField( verbose_name=_('Code'), blank=True, ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def __str__(self): return '<{}>'.format(self.code_type)
class Bootstrap3ListGroupPlugin(CMSPlugin): """ Component - List group: "Wrapper" Model http://getbootstrap.com/components/#alerts """ add_list_group_class = models.BooleanField( verbose_name='.list-group', default=True, blank=True, help_text=_( 'Adds the list-group and subsequent list-group-item classes.'), ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def get_short_description(self): instance = self.get_plugin_instance()[0] if not instance: return ugettext('<empty>') column_count = len(self.child_plugin_instances or []) column_count_str = ungettext('1 item', '%(count)i items', column_count) % { 'count': column_count } return column_count_str
class Bootstrap3ResponsivePlugin(CMSPlugin): """ CSS - Responsive: "Utilities" Model http://getbootstrap.com/css/#responsive-utilities """ device_breakpoints = model_fields.Responsive( verbose_name=_('Devices'), default='visible-xs visible-sm visible-md visible-lg', ) print_breakpoints = model_fields.ResponsivePrint( verbose_name=_('Print'), default='visible-print', ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def __str__(self): text = self.device_breakpoints if self.print_breakpoints: text += ' {}'.format(self.print_breakpoints) return text
class Boostrap3LabelPlugin(CMSPlugin): """ Component - Label: Model http://getbootstrap.com/components/#labels """ label = models.CharField( verbose_name=_('Label'), blank=True, default='', max_length=255, ) context = model_fields.Context( verbose_name=_('Context'), choices=(('default', _('Default')), ) + constants.CONTEXT_CHOICES, default='default', blank=False, ) classes = model_fields.Classes() attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class'], ) cmsplugin_ptr = model_fields.CMSPluginField() def __str__(self): return self.label
class ExpanderPluginConfig(CMSPlugin): text = models.CharField(max_length=255, blank=True, null=True) text_class = models.CharField(max_length=50, choices=TEXT_CLASSES, blank=True, null=True) background_class = models.CharField(max_length=50, choices=BACKGROUND_CLASSES, blank=True, null=True) attributes = AttributesField() @property def slug(self): return "expander-%s" % self.pk @property def class_str(self): class_list = [] if self.text_class: class_list.append(self.text_class) if self.background_class: class_list.append(self.background_class) return " ".join(class_list) def __str__(self): return "Expander: %s" % self.text def get_short_description(self): return "Expander: %s" % self.text
class BlockTagPluginConfig(CMSPlugin): tag_type = models.CharField(max_length=50, choices=BLOCK_TAGS, blank=True, null=True) text_class = models.CharField(max_length=50, choices=TEXT_CLASSES, blank=True, null=True) background_class = models.CharField(max_length=50, choices=BACKGROUND_CLASSES, blank=True, null=True) show_background_image = models.BooleanField(default=False) background_image = FilerImageField(blank=True, null=True, related_name="bt_bg_image", on_delete=DO_NOTHING) background_size_class = models.CharField(max_length=30, default=BACKGROUND_SIZE_COVER, choices=BACKGROUND_SIZE_CHOICES) attributes = AttributesField(excluded_keys=("class", "style")) @property def class_str(self): class_list = [] if self.text_class: class_list.append(self.text_class) if self.background_class: class_list.append(self.background_class) return " ".join(class_list)
class PersonaliseByParametersPluginModel(PersonaliseByPluginBaseModel): parameters = AttributesField() @property def configuration_string(self): query = QueryDict('', mutable=True) query.update(self.parameters) return '?{}'.format(query.urlencode())
class HTMLText(CMSPlugin): tag = models.CharField( verbose_name=_("HTML Tag"), max_length=50, choices=HTMLTag.get_choices(), default=HTMLTag.DIV) value = models.TextField(verbose_name=_("Value"), blank=True, null=True) attributes = AttributesField(verbose_name=_("Tag Attributes"), blank=True) def __str__(self): return Truncator(self.value).chars(50) if self.value else ugettext("Empty")
class List(CMSPlugin): LIST_BLANK_CHOICE = ("None", None) ORDERED_LIST = ("ol", "Ordered List") UNORDERED_LIST = ("ul", "Unordered List") LIST_TYPES = (ORDERED_LIST, UNORDERED_LIST) LIST_STYLE_DISC = ("disc", "Disc") LIST_STYLE_CIRCLE = ("circle", "Circle") LIST_STYLE_SQUARE = ("square", "Square") LIST_STYLE_NONE = ("None", "none") LIST_STYLE_CHOICES = (LIST_BLANK_CHOICE, LIST_STYLE_DISC, LIST_STYLE_CIRCLE) ORDERED_LIST_NUMBERED = ("1", "Numbers") ORDERED_LIST_UPPERCASE_LETTERS = ("A", "Uppercase Letters") ORDERED_LIST_LOWERCASE_LETTERS = ("a", "Lowercase Letters") ORDERED_LIST_ROMAN_NUMERALS = ("I", "Uppercase Roman Numerals") ORDERED_LIST_ROMAN_NUMERALS_LC = ("i", "Lowercase Roman Numerals") ORDERED_LIST_TYPES = ( LIST_BLANK_CHOICE, ORDERED_LIST_NUMBERED, ORDERED_LIST_UPPERCASE_LETTERS, ORDERED_LIST_LOWERCASE_LETTERS, ORDERED_LIST_ROMAN_NUMERALS, ORDERED_LIST_ROMAN_NUMERALS_LC, ) list_type = models.CharField( max_length=10, blank=False, null=False, default=UNORDERED_LIST[0], choices=LIST_TYPES, ) list_style_type = models.CharField( max_length=16, blank=True, null=True, default=LIST_BLANK_CHOICE[0], choices=LIST_STYLE_CHOICES, ) ordered_list_type = models.CharField( max_length=64, blank=True, null=True, default=LIST_BLANK_CHOICE[0], choices=ORDERED_LIST_TYPES, ) ordered_list_start = models.IntegerField(blank=True, null=True, default=None) attributes = AttributesField() def __str__(self): return "List %s" % str(self.pk) def get_short_description(self): return "List %s" % str(self.pk)
class DIVPluginConfig(CMSPlugin): title = models.CharField(max_length=200, blank=True, null=True) attributes = AttributesField() def __str__(self): return "Div Plugin %s" % str(self.pk) def get_short_description(self): return "Div Plugin %s" % str(self.title)
class LinkMixin(models.Model): link_url = models.URLField(_("link"), blank=True, default='') link_page = cms.models.fields.PageField( verbose_name=_("page"), blank=True, null=True, on_delete=models.SET_NULL ) link_file = filer.fields.file.FilerFileField( verbose_name=_("file"), null=True, blank=True, on_delete=models.SET_NULL, ) link_anchor = models.CharField( _("anchor"), max_length=128, blank=True, help_text=_("Adds this value as an anchor (#my-anchor) to the link."), ) link_mailto = models.EmailField( _("mailto"), blank=True, null=True, max_length=254 ) link_phone = models.CharField( _('Phone'), blank=True, null=True, max_length=40, ) link_target = models.CharField( _("target"), blank=True, max_length=100, choices=(( ("", _("same window")), ("_blank", _("new window")), ("_parent", _("parent window")), ("_top", _("topmost frame")), )) ) # Override this property in concrete classes as required. excluded_attr_keys = ['href', 'target', ] link_attributes = AttributesField( _('Link Attributes'), excluded_keys=excluded_attr_keys, blank=True) class Meta: abstract = True def get_link_url(self): if self.link_phone: link = u"tel://{0}".format(self.link_phone).replace(' ', '') elif self.link_mailto: link = u"mailto:{0}".format(self.link_mailto) elif self.link_url: link = self.link_url elif self.link_page_id: link = self.link_page.get_absolute_url() elif self.link_file: link = self.link_file.url else: link = "" if self.link_anchor: link += '#{0}'.format(self.link_anchor) return link
class Section(CMSPlugin): name = models.CharField('Section Name', max_length=25, default='', help_text='Descriptive name [not rendered on page]', blank=True, null=True) min_height = models.CharField('Minimum Section Height', max_length=25, default='0px', help_text='0 is default. Set it larger to expand height of section.') bg_image = FilerImageField( verbose_name=_('Background Image'), blank=True, null=True, on_delete=models.SET_NULL, related_name='+', ) bg_external_image = models.URLField( verbose_name=_('External URL Background Image'), blank=True, max_length=255, help_text=_('If provided, overrides the embedded image.') ) bg_color = models.CharField('CSS Background Color', max_length=25, default='transparent', help_text='(e.g., #RRGGBB, rgba(120,120,120,0.3))') bg_size = models.CharField('Background Size', max_length=25, choices=SIZE_CHOICES, default='cover') bg_position = models.CharField('Background Position', max_length=25, default='center', blank=True) bg_repeat = models.CharField('Background Repeat', max_length=25, choices=REPEAT_CHOICES, default='no-repeat', blank=True) bg_attachment = models.CharField('Background Attachment', max_length=25, choices=ATTACHMENT_CHOICES, default='scroll', blank=True) container = models.CharField( verbose_name='Add nested .container or .container-fluid child element', max_length=25, choices=CONTAINER_CHOICES, blank=True, default='container', help_text=_('Adds a ".container" or ".container-fluid" element inside the section. ' 'Use it to have a full-page-width styled <section> with an inner container with ' 'a constrained width. All child plugins render inside the container.'), ) classes = Classes() attributes = AttributesField( verbose_name='Attributes', blank=True, excluded_keys=['class'], ) def __str__(self): return self.name + ' ' + self.container @property def bg_image_url(self): if self.bg_image: return self.bg_image.url else: return self.bg_external_image def clean(self): # you shall only set one image kind if self.bg_image and self.bg_external_image: raise ValidationError( ugettext('You need to add either an image or a URL ' 'linking to an external image, not both.') )
class GatedContentContainerPlugin(CMSPlugin): attributes = AttributesField( verbose_name=_('Attributes'), blank=True, ) cmsplugin_ptr = CMSPluginField() def __str__(self): return str(self.pk)
class BaseObjectWithAttributes(models.Model): attributes = AttributesField( _('Attributes'), blank=True, excluded_keys=['href', 'target'], ) class Meta: abstract = True
class VersionedVideoPlayer(CMSPlugin): template = models.CharField( verbose_name=_('Template'), choices=get_templates(), default=get_templates()[0][0], max_length=255, ) label = models.CharField( verbose_name=_('Label'), blank=True, max_length=255, ) embed_link = models.CharField( verbose_name=_('Embed link'), blank=True, max_length=255, help_text=_( 'Use this field to embed videos from external services ' 'such as YouTube, Vimeo or others. Leave it blank to upload video ' 'files by adding nested "Source" plugins.'), ) poster_grouper = ImageGrouperField( verbose_name=_('Poster'), blank=True, null=True, on_delete=models.SET_NULL, related_name='+', ) attributes = AttributesField( verbose_name=_('Attributes'), blank=True, ) # Add an app namespace to related_name to avoid field name clashes # with any other plugins that have a field with the same name as the # lowercase of the class name of this model. # https://github.com/divio/django-cms/issues/5030 cmsplugin_ptr = models.OneToOneField( CMSPlugin, related_name='%(app_label)s_%(class)s', on_delete=models.CASCADE, parent_link=True, ) def __str__(self): return self.label or self.embed_link or str(self.pk) @cached_property def poster(self): if self.poster_grouper_id: return self.poster_grouper.file def copy_relations(self, oldinstance): # Because we have a ForeignKey, it's required to copy over # the reference from the instance to the new plugin. self.poster = oldinstance.poster
class DeviceMockup(CMSPlugin): device = models.CharField( verbose_name=_('Device'), max_length=100, ) orientation = models.CharField( verbose_name=_('Orientation'), blank=True, null=True, max_length=100, ) colour = models.CharField( verbose_name=_('Colour'), blank=True, null=True, max_length=100, ) # Default id/classes/attributes # -------------------------------- id_name = models.CharField( verbose_name=_('ID name'), blank=True, null=True, max_length=255, ) additional_classes = models.CharField( verbose_name=_('Additional classes'), blank=True, max_length=255, help_text=_('Additional comma separated list of classes ' 'to be added to the element e.g. "row, column-12, clearfix".'), ) attributes = AttributesField( verbose_name=_('Attributes'), blank=True, excluded_keys=['class', 'id', 'style'], ) def clean(self): if self.id_name: valid_id_name = slugify(self.id_name) self.id_name = valid_id_name def get_additional_classes(self): return ' '.join(item.strip() for item in self.additional_classes.split(',') if item.strip()) @property def get_id_name(self): return self.id_name or str(self.pk) def __str__(self): return self.get_id_name
class SectionPluginConfig(CMSPlugin): title = models.CharField(max_length=200, blank=True, null=True) blue_background = models.BooleanField(default=False) attributes = AttributesField() def __str__(self): return "Section Plugin %s" % str(self.pk) def get_short_description(self): return "Section Plugin %s" % str(self.pk)
def test_excluded_keys(self): # First prove that the keys we're about to test would normally pass field = AttributesField() try: field.validate_key('href') field.validate_key('src') except ValidationError: self.fail('Keys that pass have failed.') # Now show that they no longer pass if explicitly exclude field = AttributesField(excluded_keys=[ 'href', 'src', ]) with self.assertRaises(ValidationError): field.validate_key('href') with self.assertRaises(ValidationError): field.validate_key('src')
class JSLinkList(CMSPlugin): title = models.CharField(max_length=255, blank=True) layout = models.CharField(blank=True, default='', max_length=60, verbose_name=_('layout')) attributes = AttributesField(verbose_name='Attributes', blank=True) def __str__(self): return Truncator(strip_tags(self.title)).words(3, truncate="...")