class FilterableListForm(forms.Form): title_attrs = {'placeholder': 'Search for a specific word in item title'} topics_select_attrs = { 'multiple': 'multiple', 'data-placeholder': 'Search for topics', } authors_select_attrs = { 'multiple': 'multiple', 'data-placeholder': 'Search for authors' } title = forms.CharField(max_length=250, required=False, widget=widgets.TextInput(attrs=title_attrs)) from_date = FilterableFromDateField() to_date = FilterableToDateField() categories = forms.MultipleChoiceField( required=False, choices=ref.page_type_choices, widget=widgets.CheckboxSelectMultiple()) topics = MultipleChoiceFieldNoValidation( required=False, choices=[], widget=widgets.SelectMultiple(attrs=topics_select_attrs)) authors = forms.MultipleChoiceField( required=False, choices=[], widget=widgets.SelectMultiple(attrs=authors_select_attrs)) def __init__(self, *args, **kwargs): self.hostname = kwargs.pop('hostname') self.base_query = kwargs.pop('base_query') super(FilterableListForm, self).__init__(*args, **kwargs) pages = self.base_query.live_shared(self.hostname) page_ids = pages.values_list('id', flat=True) clean_categories(selected_categories=self.data.get('categories')) self.set_topics(page_ids) self.set_authors(page_ids) def get_page_set(self): query = self.generate_query() return self.base_query.filter(query).distinct().order_by( '-date_published') def prepare_options(self, arr): """ Returns an ordered list of tuples of the format ('tag-slug-name', 'Tag Display Name') """ arr = Counter(arr).most_common() # Order by most to least common # Grab only the first tuple in the generated tuple, # which includes a count we do not need return [x[0] for x in arr] # Populate Topics' choices def set_topics(self, page_ids): tags = Tag.objects.filter( v1_cfgovtaggedpages_items__content_object__id__in=page_ids ).values_list('slug', 'name') options = self.prepare_options(arr=tags) most = options[:3] other = options[3:] self.fields['topics'].choices = \ (('Most frequent', most), ('All other topics', other)) # Populate Authors' choices def set_authors(self, page_ids): authors = Tag.objects.filter( v1_cfgovauthoredpages_items__content_object__id__in=page_ids ).values_list('slug', 'name') options = self.prepare_options(arr=authors) self.fields['authors'].choices = options def clean(self): cleaned_data = super(FilterableListForm, self).clean() from_date = cleaned_data.get('from_date') to_date = cleaned_data.get('to_date') # Check if both date_lte and date_gte are present # If the 'start' date is after the 'end' date, swap them if (from_date and to_date) and to_date < from_date: data = dict(self.data) data_to_date = data['to_date'] self.cleaned_data['to_date'], data['to_date'] = \ from_date, data['from_date'] self.cleaned_data['from_date'], data['from_date'] = \ to_date, data_to_date self.data = data return self.cleaned_data # Does the job of {{ field }} # In the template, you pass the field and the id and name you'd like to # render the field with. def render_with_id(self, field, attr_id): for f in self.fields: if field.html_name == f: self.fields[f].widget.attrs.update({'id': attr_id}) self.set_field_html_name(self.fields[f], attr_id) return self[f] # Sets the html name by replacing the render method to use the given name. def set_field_html_name(self, field, new_name): """ This creates wrapper around the normal widget rendering, allowing for a custom field name (new_name). """ old_render = field.widget.render if isinstance(field.widget, widgets.SelectMultiple): field.widget.render = lambda name, value, attrs=None, choices=(): \ old_render(new_name, value, attrs, choices) else: field.widget.render = lambda name, value, attrs=None: \ old_render(new_name, value, attrs) # Generates a query by iterating over the zipped collection of # tuples. def generate_query(self): final_query = Q() if self.is_bound: for query, field_name in zip(self.get_query_strings(), self.declared_fields): if self.cleaned_data.get(field_name): final_query &= \ Q((query, self.cleaned_data.get(field_name))) return final_query # Returns a list of query strings to associate for each field, ordered by # the field declaration for the form. Note: THEY MUST BE ORDERED IN THE # SAME WAY AS THEY ARE DECLARED IN THE FORM DEFINITION. def get_query_strings(self): return [ 'title__icontains', # title 'date_published__gte', # from_date 'date_published__lte', # to_date 'categories__name__in', # categories 'tags__slug__in', # topics 'authors__slug__in', # authors ]
class BootstrapPicturePlugin(ImageAnnotationMixin, LinkPluginBase): name = _("Picture") model_mixins = ( ImagePropertyMixin, LinkElementMixin, ) module = 'Bootstrap' parent_classes = ['BootstrapColumnPlugin', 'SimpleWrapperPlugin'] require_parent = True allow_children = False raw_id_fields = ('image_file', ) admin_preview = False ring_plugin = 'PicturePlugin' render_template = 'cascade/bootstrap3/linked-picture.html' default_css_class = 'img-responsive' default_css_attributes = ('image_shapes', ) html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'} fields = ('image_file', ) + LinkPluginBase.fields RESIZE_OPTIONS = ( ('upscale', _("Upscale image")), ('crop', _("Crop image")), ('subject_location', _("With subject location")), ('high_resolution', _("Optimized for Retina")), ) responsive_heights = GlossaryField( MultipleCascadingSizeWidget(BS3_BREAKPOINT_KEYS, allowed_units=['px', '%'], required=False), label=_("Adapt Picture Heights"), initial={ 'xs': '100%', 'sm': '100%', 'md': '100%', 'lg': '100%' }, help_text= _("Heights of picture in percent or pixels for distinct Bootstrap's breakpoints." ), ) responsive_zoom = GlossaryField( MultipleCascadingSizeWidget(BS3_BREAKPOINT_KEYS, allowed_units=['%'], required=False), label=_("Adapt Picture Zoom"), initial={ 'xs': '0%', 'sm': '0%', 'md': '0%', 'lg': '0%' }, help_text= _("Magnification of picture in percent for distinct Bootstrap's breakpoints." ), ) resize_options = GlossaryField( widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['subject_location', 'high_resolution']) class Media: js = ['cascade/js/admin/pictureplugin.js'] def get_form(self, request, obj=None, **kwargs): reduce_breakpoints(self, 'responsive_heights') image_file = ModelChoiceField(queryset=Image.objects.all(), required=False, label=_("Image")) Form = type( str('ImageForm'), ( ImageFormMixin, getattr(LinkForm, 'get_form_class')(), ), { 'LINK_TYPE_CHOICES': ImageFormMixin.LINK_TYPE_CHOICES, 'image_file': image_file }) kwargs.update(form=Form) return super(BootstrapPicturePlugin, self).get_form(request, obj, **kwargs) def render(self, context, instance, placeholder): # image shall be rendered in a responsive context using the picture element elements = get_picture_elements(context, instance) fluid = instance.get_complete_glossary().get('fluid') == 'on' context.update({ 'is_responsive': True, 'instance': instance, 'is_fluid': fluid, 'placeholder': placeholder, 'elements': elements, }) return context @classmethod def get_css_classes(cls, obj): css_classes = super(BootstrapPicturePlugin, cls).get_css_classes(obj) css_class = obj.glossary.get('css_class') if css_class: css_classes.append(css_class) return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapPicturePlugin, cls).get_identifier(obj) try: content = force_text(obj.image) except AttributeError: content = _("No Picture") return format_html('{0}{1}', identifier, content)
class BaseForm(forms.Form): """ required:是否必填 error_messages:錯誤提示(要透過{{ obj.errors.user.0 }}調用) widget:訂製html插件(重要) label:訂製標籤名(要透過{{ obj.user.label }}調用) initial:默認值 validators:自訂製驗證規則 disabled:是否禁止編輯 """ name = forms.CharField( initial="jamie", max_length=32, min_length=3, required=True, error_messages={ "invalid": "請輸入正確格式", # 所有格式錯誤都用invalid "min_length": "請輸入最少3個字元", }) age = forms.IntegerField(initial=18, max_value=100, min_value=0, error_messages={ "invalid": "請輸入數字格式", "min_value": "最好會有負的年齡", }) email = forms.EmailField(initial="*****@*****.**", ) tall = forms.DecimalField(initial=178.87, ) file = forms.FileField(required=False, ) # 單選 city = fields.ChoiceField( choices=[(1, "台北"), (2, "台中"), (3, "高雄")], # (value, 顯示文字) initial=3, # 默認value=3 ) # 多選 hobby = fields.MultipleChoiceField( choices=[(1, "籃球"), (2, "電動"), (3, "小說")], # (value, 顯示文字) initial=[1, 2], ) birth = fields.DateField(initial="2000-01-01") # 格式:2015-09-01 # datetime = fields.DateTimeField() # 格式:2015-09-01 11:12 radio = fields.ChoiceField( choices=[(1, 'man'), (2, 'woman')], widget=widgets.RadioSelect(), initial=1, ) checkbox = fields.MultipleChoiceField( choices=[(1, 'man'), (2, 'woman')], widget=widgets.CheckboxSelectMultiple(), initial=[1, 2], ) myField = fields.CharField( # 自定義正則(一) # validators可設置多個RegexValidator # RegexValidator(正則, 錯誤信息) validators=[ RegexValidator(r"^[0-9]+$", "請輸入數字"), RegexValidator(r"^87[0-9]+$", "請以87開頭") ], initial="87123", ) # 自定義正則(二) # fields.RegexField(正則),只可設置單個正則 myField2 = fields.RegexField( r"^[0-9]+$", error_messages={ "invalid": "請輸入數字", }, initial="123456", )
def get_form(self, request, obj=None, **kwargs): from cmsplugin_cascade.models import PluginExtraFields from .config import PluginExtraFieldsConfig glossary_fields = list( kwargs.pop('glossary_fields', self.glossary_fields)) clsname = self.__class__.__name__ try: site = get_current_site(request) extra_fields = PluginExtraFields.objects.get(plugin_type=clsname, site=site) except ObjectDoesNotExist: extra_fields = app_settings.CMSPLUGIN_CASCADE[ 'plugins_with_extra_fields'].get(clsname) if isinstance(extra_fields, (PluginExtraFields, PluginExtraFieldsConfig)): # add a text input field to let the user name an ID tag for this HTML element if extra_fields.allow_id_tag: glossary_fields.append( GlossaryField(widgets.TextInput(), label=_("Named Element ID"), name='extra_element_id')) # add a select box to let the user choose one or more CSS classes class_names = extra_fields.css_classes.get('class_names', '').replace(' ', '') if class_names: choices = [(clsname, clsname) for clsname in class_names.split(',')] if extra_fields.css_classes.get('multiple'): widget = widgets.CheckboxSelectMultiple(choices=choices) else: widget = widgets.Select( choices=((None, _("Select CSS")), ) + tuple(choices)) glossary_fields.append( GlossaryField( widget, label=_("Customized CSS Classes"), name='extra_css_classes', help_text= _("Customized CSS classes to be added to this element." ))) # add input fields to let the user enter styling information for style, choices_tuples in app_settings.CMSPLUGIN_CASCADE[ 'extra_inline_styles'].items(): inline_styles = extra_fields.inline_styles.get( 'extra_fields:{0}'.format(style)) if not inline_styles: continue Widget = choices_tuples[1] if issubclass(Widget, MultipleCascadingSizeWidget): key = 'extra_inline_styles:{0}'.format(style) allowed_units = extra_fields.inline_styles.get( 'extra_units:{0}'.format(style)).split(',') widget = Widget(inline_styles, allowed_units=allowed_units, required=False) glossary_fields.append( GlossaryField(widget, label=style, name=key)) else: for inline_style in inline_styles: key = 'extra_inline_styles:{0}'.format(inline_style) label = '{0}: {1}'.format(style, inline_style) glossary_fields.append( GlossaryField(Widget(), label=label, name=key)) kwargs.update(glossary_fields=glossary_fields) return super(ExtraFieldsMixin, self).get_form(request, obj, **kwargs)
class BootstrapJumbotronPlugin(BootstrapPluginBase): name = _("Jumbotron") model_mixins = (ContainerGridMixin, ImagePropertyMixin, ImageBackgroundMixin) form = JumbotronPluginForm require_parent = False parent_classes = ('BootstrapColumnPlugin',) allow_children = True alien_child_classes = True raw_id_fields = ['image_file'] fields = ['glossary', 'image_file'] render_template = 'cascade/bootstrap4/jumbotron.html' ring_plugin = 'JumbotronPlugin' ATTACHMENT_CHOICES = ['scroll', 'fixed', 'local'] VERTICAL_POSITION_CHOICES = ['top', '10%', '20%', '30%', '40%', 'center', '60%', '70%', '80%', '90%', 'bottom'] HORIZONTAL_POSITION_CHOICES = ['left', '10%', '20%', '30%', '40%', 'center', '60%', '70%', '80%', '90%', 'right'] REPEAT_CHOICES = ['repeat', 'repeat-x', 'repeat-y', 'no-repeat'] SIZE_CHOICES = ['auto', 'width/height', 'cover', 'contain'] container_glossary_fields = ( GlossaryField( ContainerBreakpointsWidget(choices=get_widget_choices()), label=_("Available Breakpoints"), name='breakpoints', initial=app_settings.CMSPLUGIN_CASCADE['bootstrap4']['default_bounds'].keys(), help_text=_("Supported display widths for Bootstrap's grid system.") ), GlossaryField( MultipleCascadingSizeWidget([bp.name for bp in Breakpoint], allowed_units=['px', '%'], required=False), label=_("Adapt Picture Heights"), name='container_max_heights', initial={'xs': '100%', 'sm': '100%', 'md': '100%', 'lg': '100%', 'xl': '100%'}, help_text=_("Heights of picture in percent or pixels for distinct Bootstrap's breakpoints.") ), GlossaryField( widgets.CheckboxSelectMultiple(choices=BootstrapPicturePlugin.RESIZE_OPTIONS), label=_("Resize Options"), name='resize_options', initial=['crop', 'subject_location', 'high_resolution'], help_text=_("Options to use when resizing the image.") ), ) background_color = GlossaryField( ColorPickerWidget(), label=_("Background color"), ) background_repeat = GlossaryField( widgets.RadioSelect(choices=[(c, c) for c in REPEAT_CHOICES]), initial='no-repeat', label=_("This property specifies how an image repeates."), ) background_attachment = GlossaryField( widgets.RadioSelect(choices=[(c, c) for c in ATTACHMENT_CHOICES]), initial='local', label=_("This property specifies how to move the background relative to the viewport."), ) background_vertical_position = GlossaryField( widgets.Select(choices=[(c, c) for c in VERTICAL_POSITION_CHOICES]), initial='center', label=_("This property moves a background image vertically within its container."), ) background_horizontal_position = GlossaryField( widgets.Select(choices=[(c, c) for c in HORIZONTAL_POSITION_CHOICES]), initial='center', label=_("This property moves a background image horizontally within its container."), ) background_size = GlossaryField( widgets.RadioSelect(choices=[(c, c) for c in SIZE_CHOICES]), initial='auto', label=_("Background size"), help_text=_("This property specifies how an image is sized."), ) background_width_height = GlossaryField( MultipleCascadingSizeWidget(['width', 'height'], allowed_units=['px', '%'], required=False), label=_("Background width and height"), help_text=_("This property specifies the width and height of a background image."), ) footnote_html = """ <p>For more information about the Jumbotron please read </p> """ class Media: js = ['cascade/js/admin/jumbotronplugin.js'] def get_form(self, request, obj=None, **kwargs): if self.get_parent_instance(request, obj) is None: # we only ask for breakpoints, if the jumbotron is the root of the placeholder kwargs.update(glossary_fields=list(self.container_glossary_fields)) kwargs['glossary_fields'].extend(self.glossary_fields) form = super(BootstrapJumbotronPlugin, self).get_form(request, obj, **kwargs) return form def render(self, context, instance, placeholder): # image shall be rendered in a responsive context using the ``<picture>`` element try: elements = get_picture_elements(instance) except Exception as exc: logger.warning("Unable generate picture elements. Reason: {}".format(exc)) else: if instance.child_plugin_instances and instance.child_plugin_instances[0].plugin_type == 'BootstrapRowPlugin': padding='padding: {0}px {0}px;'.format(int( app_settings.CMSPLUGIN_CASCADE['bootstrap4']['gutter']/2)) context.update({'add_gutter_if_child_is_BootstrapRowPlugin': padding,}) context.update({ 'elements': [e for e in elements if 'media' in e] if elements else [], 'CSS_PREFIXES': app_settings.CSS_PREFIXES, }) return self.super(BootstrapJumbotronPlugin, self).render(context, instance, placeholder) @classmethod def sanitize_model(cls, obj): sanitized = False # if the jumbotron is the root of the placeholder, we consider it as "fluid" obj.glossary['fluid'] = obj.parent is None super(BootstrapJumbotronPlugin, cls).sanitize_model(obj) grid_container = obj.get_bound_plugin().get_grid_instance() obj.glossary.setdefault('media_queries', {}) for bp, bound in grid_container.bounds.items(): obj.glossary['media_queries'].setdefault(bp.name, {}) width = round(bound.max) if obj.glossary['media_queries'][bp.name].get('width') != width: obj.glossary['media_queries'][bp.name]['width'] = width sanitized = True if obj.glossary['media_queries'][bp.name].get('media') != bp.media_query: obj.glossary['media_queries'][bp.name]['media'] = bp.media_query sanitized = True return sanitized @classmethod def get_css_classes(cls, obj): css_classes = cls.super(BootstrapJumbotronPlugin, cls).get_css_classes(obj) if obj.glossary.get('fluid'): css_classes.append('jumbotron-fluid') else: css_classes.append('jumbotron') return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapJumbotronPlugin, cls).get_identifier(obj) try: content = obj.image.name or obj.image.original_filename except AttributeError: content = _("Without background image") return format_html('{0}{1}', identifier, content)
class BootstrapGalleryPlugin(CascadePluginBase): name = _("Gallery") module = 'Bootstrap' intro_html = _( "<p>Add images, which make up a set to be used as gallery.</p>" "<p>All thumbnails for these images are resized to the same widths and heights.</p>" ) parent_classes = ['BootstrapColumnPlugin'] require_parent = True allow_children = False raw_id_fields = ('image_file', ) text_enabled = True admin_preview = False render_template = 'cascade/bootstrap3/gallery.html' default_css_attributes = ('image_shapes', ) html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'} inlines = (GalleryPluginInline, ) SHAPE_CHOICES = (('img-responsive', _("Responsive")), ) RESIZE_OPTIONS = ( ('upscale', _("Upscale image")), ('crop', _("Crop image")), ('subject_location', _("With subject location")), ('high_resolution', _("Optimized for Retina")), ) image_shapes = GlossaryField( widgets.CheckboxSelectMultiple(choices=SHAPE_CHOICES), label=_("Image Responsiveness"), initial=['img-responsive'], ) image_width_responsive = GlossaryField( CascadingSizeWidget(allowed_units=['%'], required=False), label=_("Responsive Image Width"), initial='100%', help_text=_( "Set the image width in percent relative to containing element."), ) image_width_fixed = GlossaryField( CascadingSizeWidget(allowed_units=['px'], required=False), label=_("Fixed Image Width"), help_text=_("Set a fixed image width in pixels."), ) image_height = GlossaryField( CascadingSizeWidget(allowed_units=['px', '%'], required=False), label=_("Adapt Image Height"), help_text= _("Set a fixed height in pixels, or percent relative to the image width." ), ) thumbnail_width = GlossaryField( CascadingSizeWidget(allowed_units=['px']), label=_("Thumbnail Width"), help_text=_("Set a fixed thumbnail width in pixels."), ) thumbnail_height = GlossaryField( CascadingSizeWidget(allowed_units=['px', '%']), label=_("Thumbnail Height"), help_text= _("Set a fixed height in pixels, or percent relative to the thumbnail width." ), ) resize_options = GlossaryField( widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['crop', 'subject_location', 'high_resolution'], ) class Media: js = resolve_dependencies('cascade/js/admin/imageplugin.js') def get_form(self, request, obj=None, **kwargs): utils.reduce_breakpoints(self, 'responsive_heights') form = super(BootstrapGalleryPlugin, self).get_form(request, obj, **kwargs) return form def render(self, context, instance, placeholder): gallery_instances = [] options = dict(instance.get_complete_glossary()) for inline_element in instance.sortinline_elements.all(): # since inline_element requires the property `image`, add ImagePropertyMixin # to its class during runtime try: ProxyModel = create_proxy_model('GalleryImage', (ImagePropertyMixin, ), SortableInlineCascadeElement, module=__name__) inline_element.__class__ = ProxyModel options.update( inline_element.glossary, **{ 'image_width_fixed': options['thumbnail_width'], 'image_height': options['thumbnail_height'], 'is_responsive': False, }) thumbnail_tags = utils.get_image_tags(context, inline_element, options) for key, val in thumbnail_tags.items(): setattr(inline_element, key, val) gallery_instances.append(inline_element) except (KeyError, AttributeError): pass inline_styles = instance.glossary.get('inline_styles', {}) inline_styles.update(width=options['thumbnail_width']) instance.glossary['inline_styles'] = inline_styles context.update( dict(instance=instance, placeholder=placeholder, gallery_instances=gallery_instances)) return context @classmethod def get_css_classes(cls, obj): css_classes = super(BootstrapGalleryPlugin, cls).get_css_classes(obj) css_class = obj.glossary.get('css_class') if css_class: css_classes.append(css_class) return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapGalleryPlugin, cls).get_identifier(obj) num_elems = obj.sortinline_elements.count() content = ungettext_lazy("with {0} image", "with {0} images", num_elems).format(num_elems) return format_html('{0}{1}', identifier, content)
class CompanyForm(MaterialModelForm): use_required_attribute = False categories = TaxonomyModelChoiceField( queryset=CompanyProxy.list_categories(), widget=widgets.CheckboxSelectMultiple(attrs={'class': 'hidden'})) communities = TaxonomyModelChoiceField( queryset=CompanyProxy.list_communities(), widget=widgets.SelectMultiple(attrs={'class': 'shows'})) email = forms.EmailField(required=True, label=_("Email")) request_user = None def is_valid(self): valid = super(CompanyForm, self).is_valid() if 'email' in self.cleaned_data: email = self.cleaned_data['email'] User = get_user_model() emails_exists = User.objects.filter(email=email).exists() if emails_exists and ( (self.instance.user and self.instance.user.email != email) or not self.instance.id): self.add_error( 'email', ValidationError(('Email already exists.'), code='email_exists')) valid = False return valid def __init__(self, *args, **kwargs): instance = kwargs.get('instance') if instance: initial = kwargs.get('initial', {}) initial.update({ 'categories': instance.taxonomies.filter(term__slug='categoria'), 'communities': instance.taxonomies.filter(term__slug='comunidade'), 'email': instance.user.email }) kwargs.update({'initial': initial}) super(CompanyForm, self).__init__(*args, **kwargs) self.fields['communities'].widget.label = _("Comunidades") class Meta: model = CompanyProxy exclude = ('user', 'taxonomies', 'members') labels = { 'name': u'Nome da Organização', 'website': 'URL', 'city': 'Cidade' } widgets = { "logo": forms.FileInput # "name": InputTextMaterial } def set_request_user(self, user): self.request_user = user def save(self, commit=True): self.instance.email = self.cleaned_data['email'] instance = super(CompanyForm, self).save(commit=commit) return instance
class BootstrapImagePlugin(LinkPluginBase): name = _("Image") model_mixins = ( ImagePropertyMixin, LinkElementMixin, ) form = LinkedImageForm module = 'Bootstrap' parent_classes = ['BootstrapColumnPlugin'] require_parent = True allow_children = False raw_id_fields = ('image_file', ) text_enabled = True admin_preview = False render_template = 'cascade/bootstrap3/linked-image.html' default_css_attributes = ('image-shapes', ) html_tag_attributes = {'image-title': 'title', 'alt-tag': 'tag'} fields = ( 'image_file', 'glossary', ( 'link_type', 'cms_page', 'ext_url', ), ) SHAPE_CHOICES = ( ('img-responsive', _("Responsive")), ('img-rounded', _('Rounded')), ('img-circle', _('Circle')), ('img-thumbnail', _('Thumbnail')), ) RESIZE_OPTIONS = ( ('upscale', _("Upscale image")), ('crop', _("Crop image")), ('subject_location', _("With subject location")), ('high_resolution', _("Optimized for Retina")), ) glossary_fields = ( PartialFormField( 'image-title', widgets.TextInput(), label=_('Image Title'), help_text= _("Caption text added to the 'title' attribute of the <img> element." ), ), PartialFormField( 'alt-tag', widgets.TextInput(), label=_('Alternative Description'), help_text= _("Textual description of the image added to the 'alt' tag of the <img> element." ), ), ) + LinkPluginBase.glossary_fields + ( PartialFormField('image-shapes', widgets.CheckboxSelectMultiple(choices=SHAPE_CHOICES), label=_("Image Shapes"), initial=['img-responsive']), PartialFormField( 'image-width-responsive', CascadingSizeWidget(allowed_units=['%'], required=False), label=_("Responsive Image Width"), initial='100%', help_text=_( "Set the image width in percent relative to containing element." ), ), PartialFormField( 'image-width-fixed', CascadingSizeWidget(allowed_units=['px'], required=False), label=_("Fixed Image Width"), help_text=_("Set a fixed image width in pixels."), ), PartialFormField( 'image-height', CascadingSizeWidget(allowed_units=['px', '%'], required=False), label=_("Adapt Image Height"), help_text= _("Set a fixed height in pixels, or percent relative to the image width." ), ), PartialFormField( 'resize-options', widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['subject_location', 'high_resolution']), ) class Media: js = resolve_dependencies('cascade/js/admin/imageplugin.js') def get_form(self, request, obj=None, **kwargs): utils.reduce_breakpoints(self, 'responsive-heights') return super(BootstrapImagePlugin, self).get_form(request, obj, **kwargs) def render(self, context, instance, placeholder): is_responsive = 'img-responsive' in instance.glossary.get( 'image-shapes', []) tags = utils.get_image_tags(context, instance, is_responsive) if 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 = super(BootstrapImagePlugin, cls).get_css_classes(obj) css_class = obj.glossary.get('css_class') if css_class: css_classes.append(css_class) return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapImagePlugin, cls).get_identifier(obj) try: content = force_text(obj.image) except AttributeError: content = _("No Image") return format_html('{0}{1}', identifier, content)
class BootstrapContainerPlugin(BootstrapPluginBase): name = _("Container") require_parent = False parent_classes = [] form = BootstrapContainerForm breakpoints = list(BS3_BREAKPOINTS) i = 0 widget_choices = [] for br, br_options in BS3_BREAKPOINTS.items(): if i == 0: widget_choices.append( (br, '{} (<{}px)'.format(br_options[2], br_options[0]))) elif i == len(breakpoints[:-1]): widget_choices.append( (br, '{} (≥{}px)'.format(br_options[2], br_options[0]))) else: widget_choices.append((br, '{} (≥{}px and <{}px)'.format( br_options[2], br_options[0], BS3_BREAKPOINTS[breakpoints[(i + 1)]][0]))) i += 1 WIDGET_CHOICES = tuple(widget_choices) glossary_fields = ( PartialFormField( 'breakpoints', widgets.CheckboxSelectMultiple( choices=WIDGET_CHOICES, renderer=ContainerBreakpointsRenderer), label=_('Available Breakpoints'), initial=breakpoints[::-1], help_text=_( "Supported display widths for Bootstrap's grid system.")), PartialFormField( 'fluid', widgets.CheckboxInput(), label=_('Fluid Container'), initial=False, help_text=_( "Changing your outermost '.container' to '.container-fluid'.") ), ) glossary_variables = ['container_max_widths', 'media_queries'] class Media: css = { 'all': ('//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css', ) } js = ['cascade/js/admin/containerplugin.js'] @classmethod def get_identifier(cls, obj): identifier = super(BootstrapContainerPlugin, cls).get_identifier(obj) breakpoints = obj.glossary.get('breakpoints') content = obj.glossary.get('fluid') and '(fluid) ' or '' if breakpoints: devices = ', '.join( [force_text(BS3_BREAKPOINTS[bp][2]) for bp in breakpoints]) content = _("{0}for {1}").format(content, devices) return format_html('{0}{1}', identifier, content) @classmethod def get_css_classes(cls, obj): css_classes = super(BootstrapContainerPlugin, cls).get_css_classes(obj) if obj.glossary.get('fluid'): css_classes.append('container-fluid') else: css_classes.append('container') return css_classes def save_model(self, request, obj, form, change): super(BootstrapContainerPlugin, self).save_model(request, obj, form, change) obj.sanitize_children() @classmethod def sanitize_model(cls, obj): sanitized = super(BootstrapContainerPlugin, cls).sanitize_model(obj) parent_glossary = obj.get_parent_glossary() # compute the max width and the required media queries for each chosen breakpoint obj.glossary['container_max_widths'] = max_widths = {} obj.glossary['media_queries'] = media_queries = {} breakpoints = obj.glossary.get('breakpoints', []) last_index = len(breakpoints) - 1 for index, bp in enumerate(breakpoints): try: max_widths[bp] = parent_glossary['container_max_widths'][bp] except KeyError: if obj.glossary.get('fluid'): if bp == 'lg': max_widths[bp] = settings.CMSPLUGIN_CASCADE[ 'bootstrap3']['fluid-lg-width'] else: max_widths[bp] = BS3_BREAKPOINTS[bp][0] else: max_widths[bp] = BS3_BREAKPOINTS[bp][3] if last_index > 0: if index == 0: next_bp = breakpoints[1] media_queries[bp] = [ '(max-width: {0}px)'.format( BS3_BREAKPOINTS[next_bp][0]) ] elif index == last_index: media_queries[bp] = [ '(min-width: {0}px)'.format(BS3_BREAKPOINTS[bp][0]) ] else: next_bp = breakpoints[index + 1] media_queries[bp] = [ '(min-width: {0}px)'.format(BS3_BREAKPOINTS[bp][0]), '(max-width: {0}px)'.format( BS3_BREAKPOINTS[next_bp][0]) ] return sanitized def get_parent_classes(self, slot, page): if self.cms_plugin_instance and self.cms_plugin_instance.parent: # enforce that a ContainerPlugin can't have a parent return [] return super(BootstrapContainerPlugin, self).get_parent_classes(slot, page)
class CarouselPlugin(BootstrapPluginBase): name = _("Carousel") form = CarouselSlidesForm default_css_class = 'carousel' default_css_attributes = ('options', ) parent_classes = ['BootstrapColumnPlugin'] render_template = 'cascade/bootstrap3/carousel.html' default_inline_styles = {'overflow': 'hidden'} fields = ( 'num_children', 'glossary', ) DEFAULT_CAROUSEL_ATTRIBUTES = {'data-ride': 'carousel'} OPTION_CHOICES = ( ('slide', _("Animate")), ('pause', _("Pause")), ('wrap', _("Wrap")), ) glossary_fields = ( PartialFormField( 'interval', NumberInputWidget(attrs={ 'size': '2', 'style': 'width: 4em;', 'min': '1' }), label=_("Interval"), initial=5, help_text=_("Change slide after this number of seconds."), ), PartialFormField( 'options', widgets.CheckboxSelectMultiple(choices=OPTION_CHOICES), label=_('Options'), initial=['slide', 'wrap', 'pause'], help_text=_("Adjust interval for the carousel."), ), PartialFormField( 'container_max_heights', MultipleCascadingSizeWidget(CASCADE_BREAKPOINTS_LIST, allowed_units=['px']), label=_("Carousel heights"), initial={ 'xs': '100px', 'sm': '150px', 'md': '200px', 'lg': '300px' }, help_text= _("Heights of Carousel in pixels for distinct Bootstrap's breakpoints." ), ), PartialFormField( 'resize-options', widgets.CheckboxSelectMultiple( choices=BootstrapPicturePlugin.RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['upscale', 'crop', 'subject_location', 'high_resolution']), ) def get_form(self, request, obj=None, **kwargs): utils.reduce_breakpoints(self, 'container_max_heights') return super(CarouselPlugin, self).get_form(request, obj, **kwargs) @classmethod def get_identifier(cls, obj): identifier = super(CarouselPlugin, cls).get_identifier(obj) num_cols = obj.get_children().count() content = ungettext_lazy('with {0} slide', 'with {0} slides', num_cols).format(num_cols) return format_html('{0}{1}', identifier, content) @classmethod def get_css_classes(cls, obj): css_classes = super(CarouselPlugin, cls).get_css_classes(obj) if 'slide' in obj.glossary.get('options', []): css_classes.append('slide') return css_classes @classmethod def get_html_tag_attributes(cls, obj): attributes = super(CarouselPlugin, cls).get_html_tag_attributes(obj) attributes.update(cls.DEFAULT_CAROUSEL_ATTRIBUTES) attributes['data-interval'] = 1000 * int( obj.glossary.get('interval', 5)) options = obj.glossary.get('options', []) attributes['data-pause'] = 'pause' in options and 'hover' or 'false' attributes['data-wrap'] = 'wrap' in options and 'true' or 'false' return attributes def save_model(self, request, obj, form, change): wanted_children = int(form.cleaned_data.get('num_children')) super(CarouselPlugin, self).save_model(request, obj, form, change) self.extend_children(obj, wanted_children, CarouselSlidePlugin) @classmethod def sanitize_model(cls, obj): sanitized = super(CarouselPlugin, cls).sanitize_model(obj) complete_glossary = obj.get_complete_glossary() # fill all invalid heights for this container to a meaningful value max_height = max(obj.glossary['container_max_heights'].values()) pattern = re.compile(r'^(\d+)px$') for bp in complete_glossary['breakpoints']: if not pattern.match(obj.glossary['container_max_heights'].get( bp, '')): obj.glossary['container_max_heights'][bp] = max_height return sanitized
class RecipeForm(forms.ModelForm): tags = forms.ModelMultipleChoiceField( queryset=Tag.objects.all(), widget=widgets.CheckboxSelectMultiple(), ) amount = forms.MultipleChoiceField(required=False) food = forms.MultipleChoiceField(required=False) class Meta: model = Recipe fields = [ "name", "description", "cooking_time", "tags", "image", ] labels = { "name": ("Название рецепта"), "description": ("Описание рецепта"), "cooking_time": ("Время приготовления"), "tags": ("Тэги"), "image": ("Вкуная картинка"), "food": ("Ингредиенты"), "amount": ("Количество ингредиента"), } help_text = { "name": ("Придуймайте название рецепта"), "description": ("Подробно опишите процесс приголовления"), "cooking_time": ("Сколько времени всё это займет в минутах"), "tags": ("К какой категории отностися этот рецепт"), "image": ("Загрузите какое-нибудь фото"), "food": ("Укажите нежные продукты"), "amount": ("Количество продуктов"), } def clean_food(self): food_names = self.data.getlist("nameIngredient") food_units = self.data.getlist("unitsIngredient") food_amount = self.data.getlist("valueIngredient") if len(food_names) != len(food_units) != len(food_amount): raise forms.ValidationError( "Number of titles does not equal number of units and " "amount of products") food = zip(food_names, food_units) cleaned_food = {} for count, (name, unit) in enumerate(food): if int(food_amount[count]) < 0: raise ValidationError("Количество не может быть меньше 0") ingredient = get_object_or_404(Food, name=name, unit=unit) cleaned_food[ingredient] = food_amount[count] self.cleaned_data["food"] = cleaned_food return self.cleaned_data["food"] def save(self, commit=True): super().save(commit=False) self.instance.author = self.initial["author"] self.instance.save() self.instance.tags.set(self.cleaned_data["tags"]) ingredients = self.cleaned_data["food"] Ingredient.objects.filter(recipe=self.instance).exclude( food__in=ingredients.keys()).delete() for food, amount in ingredients.items(): Ingredient.objects.update_or_create(recipe=self.instance, food=food, defaults={"amount": amount}) return self.instance
class BootstrapPicturePlugin(LinkPluginBase): name = _("Picture") model_mixins = ( ImagePropertyMixin, LinkElementMixin, ) module = 'Bootstrap' parent_classes = ['BootstrapColumnPlugin', 'BootstrapPanelPlugin'] require_parent = True allow_children = False raw_id_fields = ('image_file', ) text_enabled = False admin_preview = False render_template = 'cascade/bootstrap3/linked-picture.html' default_css_class = 'img-responsive' default_css_attributes = ('image-shapes', ) html_tag_attributes = {'image-title': 'title', 'alt-tag': 'tag'} fields = ( 'image_file', getattr(LinkPluginBase, 'glossary_field_map')['link'], 'glossary', ) LINK_TYPE_CHOICES = (('none', _("No Link")),) + \ tuple(t for t in getattr(LinkForm, 'LINK_TYPE_CHOICES') if t[0] != 'email') RESIZE_OPTIONS = ( ('upscale', _("Upscale image")), ('crop', _("Crop image")), ('subject_location', _("With subject location")), ('high_resolution', _("Optimized for Retina")), ) glossary_fields = ( PartialFormField( 'image-title', widgets.TextInput(), label=_('Image Title'), help_text= _("Caption text added to the 'title' attribute of the <img> element." ), ), PartialFormField( 'alt-tag', widgets.TextInput(), label=_('Alternative Description'), help_text= _("Textual description of the image added to the 'alt' tag of the <img> element." ), ), ) + getattr(LinkPluginBase, 'glossary_fields', ()) + ( PartialFormField( 'responsive-heights', MultipleCascadingSizeWidget(CASCADE_BREAKPOINTS_LIST, allowed_units=['px', '%'], required=False), label=_("Adapt Picture Heights"), initial={ 'xs': '100%', 'sm': '100%', 'md': '100%', 'lg': '100%' }, help_text= _("Heights of picture in percent or pixels for distinct Bootstrap's breakpoints." ), ), PartialFormField( 'responsive-zoom', MultipleCascadingSizeWidget( CASCADE_BREAKPOINTS_LIST, allowed_units=['%'], required=False), label=_("Adapt Picture Zoom"), initial={ 'xs': '0%', 'sm': '0%', 'md': '0%', 'lg': '0%' }, help_text= _("Magnification of picture in percent for distinct Bootstrap's breakpoints." ), ), PartialFormField( 'resize-options', widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['subject_location', 'high_resolution']), ) class Media: js = resolve_dependencies('cascade/js/admin/pictureplugin.js') def get_form(self, request, obj=None, **kwargs): utils.reduce_breakpoints(self, 'responsive-heights') image_file = ModelChoiceField(queryset=Image.objects.all(), required=False, label=_("Image")) Form = type(str('ImageForm'), ( ImageFormMixin, getattr(LinkForm, 'get_form_class')(), ), { 'LINK_TYPE_CHOICES': self.LINK_TYPE_CHOICES, 'image_file': image_file }) kwargs.update(form=Form) return super(BootstrapPicturePlugin, self).get_form(request, obj, **kwargs) def render(self, context, instance, placeholder): # image shall be rendered in a responsive context using the picture element elements = utils.get_picture_elements(context, instance) context.update({ 'is_responsive': True, 'instance': instance, 'placeholder': placeholder, 'elements': elements, }) return context @classmethod def get_css_classes(cls, obj): css_classes = super(BootstrapPicturePlugin, cls).get_css_classes(obj) css_class = obj.glossary.get('css_class') if css_class: css_classes.append(css_class) return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapPicturePlugin, cls).get_identifier(obj) try: content = force_text(obj.image) except AttributeError: content = _("No Picture") return format_html('{0}{1}', identifier, content)
class BootstrapButtonMixin(IconPluginMixin): require_parent = True parent_classes = ( 'BootstrapColumnPlugin', 'SimpleWrapperPlugin', ) render_template = 'cascade/bootstrap3/button.html' allow_children = False default_css_class = 'btn' default_css_attributes = ( 'button_type', 'button_size', 'button_options', 'quick_float', ) ring_plugin = 'ButtonMixin' button_type = GlossaryField( ButtonTypeWidget.get_instance(), label=_("Button Type"), initial='btn-default', help_text=_("Display Link using this Button Style")) button_size = GlossaryField( ButtonSizeWidget.get_instance(), label=_("Button Size"), initial='', help_text=_("Display Link using this Button Size")) button_options = GlossaryField( widgets.CheckboxSelectMultiple(choices=( ('btn-block', _('Block level')), ('disabled', _('Disabled')), )), label=_("Button Options"), ) quick_float = GlossaryField( widgets.RadioSelect(choices=( ('', _("Do not float")), ('pull-left', _("Pull left")), ('pull-right', _("Pull right")), )), label=_("Quick Float"), initial='', help_text=_("Float the button to the left or right.")) icon_align = GlossaryField( widgets.RadioSelect(choices=( ('', _("No Icon")), ('icon-left', _("Icon placed left")), ('icon-right', _("Icon placed right")), )), label=_("Icon alignment"), initial='', help_text=_("Add an Icon before or after the button content.")) icon_font = GlossaryField( widgets.Select(), label=_("Font"), ) symbol = GlossaryField( widgets.HiddenInput(), label=_("Select Symbol"), ) class Media: js = ['cascade/js/admin/buttonmixin.js'] def render(self, context, instance, placeholder): context = super(BootstrapButtonMixin, self).render(context, instance, placeholder) try: icon_font = self.get_icon_font(instance) symbol = instance.glossary.get('symbol') except AttributeError: icon_font, symbol = None, None if icon_font and symbol: context['stylesheet_url'] = icon_font.get_stylesheet_url() mini_template = '{0}<i class="icon-{1} {2}" aria-hidden="true"></i>{3}' icon_align = instance.glossary.get('icon_align') if icon_align == 'icon-left': context['icon_left'] = format_html(mini_template, '', symbol, 'cascade-icon-left', ' ') elif icon_align == 'icon-right': context['icon_right'] = format_html(mini_template, ' ', symbol, 'cascade-icon-right', '') return context
class WorkStudyForm(_RegistrationStepFormBase, forms.ModelForm): class Meta: model = WorkStudyApplication fields = [ 'nickname_and_pronouns', 'phone_number', 'can_receive_texts_at_phone_number', 'home_timezone', 'other_timezone', 'has_been_to_conclave', 'has_done_work_study', 'student_info', 'job_preferences', 'relevant_job_experience', 'other_skills', 'other_info', ] widgets = { 'student_info': widgets.Textarea(attrs={ 'rows': 3, 'cols': None }), 'relevant_job_experience': widgets.Textarea(attrs={ 'rows': 5, 'cols': None }), 'other_skills': widgets.Textarea(attrs={ 'rows': 5, 'cols': None }), 'other_info': widgets.Textarea(attrs={ 'rows': 5, 'cols': None }), } labels = dict(zip(fields, itertools.repeat(''))) can_receive_texts_at_phone_number = YesNoRadioField(label='') has_been_to_conclave = YesNoRadioField(label='') has_done_work_study = YesNoRadioField(label='') home_timezone = ChoiceField(choices=( ('EDT', 'EDT (Eastern Daylight Time) UTC/GMT -4 hours -- ' 'this is CONCLAVE OFFICIAL TIME'), ('CDT', 'CDT (Central)'), ('MDT', 'MDT (Mountain)'), ('PDT', 'PDT (Pacific Daylight Time) UTC/GMT -7 hours -- this is where Koren, ' 'the work-study coordinator, lives!'), ('other', 'Other'), ), widget=widgets.RadioSelect) job_preferences = _PassThroughField( widget=widgets.CheckboxSelectMultiple(choices=WorkStudyJob.choices), label='')
class FilterableListForm(forms.Form): title_attrs = {'placeholder': 'Search for a specific word in item title'} topics_select_attrs = { 'multiple': 'multiple', 'data-placeholder': 'Search for topics', } authors_select_attrs = { 'multiple': 'multiple', 'data-placeholder': 'Search for authors' } from_select_attrs = { 'class': 'js-filter_range-date js-filter_range-date__gte', 'type': 'text', 'placeholder': 'mm/dd/yyyy', 'data-type': 'date' } to_select_attrs = from_select_attrs.copy() to_select_attrs.update({ 'class': 'js-filter_range-date js-filter_range-date__lte', }) title = forms.CharField(max_length=250, required=False, widget=widgets.TextInput(attrs=title_attrs)) from_date = FilterDateField( required=False, input_formats=['%m/%d/%Y'], widget=widgets.DateInput(attrs=from_select_attrs)) to_date = FilterDateField(required=False, input_formats=['%m/%d/%Y'], widget=widgets.DateInput(attrs=to_select_attrs)) categories = forms.MultipleChoiceField( required=False, choices=ref.page_type_choices, widget=widgets.CheckboxSelectMultiple()) topics = forms.MultipleChoiceField( required=False, choices=[], widget=widgets.SelectMultiple(attrs=topics_select_attrs)) authors = forms.MultipleChoiceField( required=False, choices=[], widget=widgets.SelectMultiple(attrs=authors_select_attrs)) def __init__(self, *args, **kwargs): parent = kwargs.pop('parent') hostname = kwargs.pop('hostname') super(FilterableListForm, self).__init__(*args, **kwargs) page_ids = CFGOVPage.objects.live_shared(hostname).descendant_of( parent).values_list('id', flat=True) self.set_topics(parent, page_ids, hostname) self.set_authors(parent, page_ids, hostname) # Populate Topics' choices def set_topics(self, parent, page_ids, hostname): tags = Tag.objects.filter( v1_cfgovtaggedpages_items__content_object__id__in=page_ids ).values_list('name', flat=True) # Orders by most to least common tags options = most_common(list(tags)) most = [(option, option) for option in options[:3]] other = [(option, option) for option in options[3:]] self.fields['topics'].choices = \ (('Most frequent', tuple(most)), ('All other topics', tuple(other))) # Populate Authors' choices def set_authors(self, parent, page_ids, hostname): authors = Tag.objects.filter( v1_cfgovauthoredpages_items__content_object__id__in=page_ids ).values_list('name', flat=True) # Orders by most to least common authors self.fields['authors'].choices = [ (author, author) for author in most_common(list(authors)) ] def clean(self): cleaned_data = super(FilterableListForm, self).clean() from_date = cleaned_data.get('from_date') to_date = cleaned_data.get('to_date') # Check if both date_lte and date_gte are present # If the 'start' date is after the 'end' date, swap them if (from_date and to_date) and to_date < from_date: data = dict(self.data) data_to_date = data['to_date'] self.cleaned_data['to_date'], data['to_date'] = \ from_date, data['from_date'] self.cleaned_data['from_date'], data['from_date'] = \ to_date, data_to_date self.data = data return self.cleaned_data # Does the job of {{ field }} # In the template, you pass the field and the id and name you'd like to # render the field with. def render_with_id(self, field, attr_id): for f in self.fields: if field.html_name == f: self.fields[f].widget.attrs.update({'id': attr_id}) self.set_field_html_name(self.fields[f], attr_id) return self[f] # Sets the html name by replacing the render method to use the given name. def set_field_html_name(self, field, new_name): """ This creates wrapper around the normal widget rendering, allowing for a custom field name (new_name). """ old_render = field.widget.render if isinstance(field.widget, widgets.SelectMultiple): field.widget.render = lambda name, value, attrs=None, choices=(): \ old_render(new_name, value, attrs, choices) else: field.widget.render = lambda name, value, attrs=None: \ old_render(new_name, value, attrs) # Generates a query by iterating over the zipped collection of # tuples. def generate_query(self): final_query = Q() if self.is_bound: for query, field_name in zip(self.get_query_strings(), self.declared_fields): if self.cleaned_data.get(field_name): final_query &= \ Q((query, self.cleaned_data.get(field_name))) return final_query # Returns a list of query strings to associate for each field, ordered by # the field declaration for the form. Note: THEY MUST BE ORDERED IN THE # SAME WAY AS THEY ARE DECLARED IN THE FORM DEFINITION. def get_query_strings(self): return [ 'title__icontains', # title 'date_published__gte', # from_date 'date_published__lte', # to_date 'categories__name__in', # categories 'tags__name__in', # topics 'authors__name__in', # authors ]
class BootstrapImagePlugin(ImageAnnotationMixin, LinkPluginBase): name = _("Image") model_mixins = ( ImagePropertyMixin, LinkElementMixin, ) module = 'Bootstrap' parent_classes = ['BootstrapColumnPlugin'] require_parent = True allow_children = False raw_id_fields = ['image_file'] admin_preview = False ring_plugin = 'ImagePlugin' render_template = 'cascade/bootstrap4/linked-image.html' default_css_attributes = ['image_shapes', 'image_alignment'] html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'} html_tag_attributes.update(LinkPluginBase.html_tag_attributes) fields = ['image_file'] + list(LinkPluginBase.fields) SHAPE_CHOICES = [('img-fluid', _("Responsive")), ('rounded', _('Rounded')), ('rounded-circle', _('Circle')), ('img-thumbnail', _('Thumbnail'))] RESIZE_OPTIONS = [('upscale', _("Upscale image")), ('crop', _("Crop image")), ('subject_location', _("With subject location")), ('high_resolution', _("Optimized for Retina"))] ALIGNMENT_OPTIONS = [('float-left', _("Left")), ('float-right', _("Right")), ('mx-auto', _("Center"))] image_shapes = GlossaryField( widgets.CheckboxSelectMultiple(choices=SHAPE_CHOICES), label=_("Image Shapes"), initial=['img-fluid']) image_width_responsive = GlossaryField( CascadingSizeWidget(allowed_units=['%'], required=False), label=_("Responsive Image Width"), initial='100%', help_text=_( "Set the image width in percent relative to containing element."), ) image_width_fixed = GlossaryField( CascadingSizeWidget(allowed_units=['px'], required=False), label=_("Fixed Image Width"), help_text=_("Set a fixed image width in pixels."), ) image_height = GlossaryField( CascadingSizeWidget(allowed_units=['px', '%'], required=False), label=_("Adapt Image Height"), help_text= _("Set a fixed height in pixels, or percent relative to the image width." ), ) resize_options = GlossaryField( widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['subject_location', 'high_resolution'], ) image_alignment = GlossaryField( widgets.RadioSelect(choices=ALIGNMENT_OPTIONS), label=_("Image Alignment"), help_text=_("How to align a non-responsive image."), ) class Media: js = ['cascade/js/admin/imageplugin.js'] def get_form(self, request, obj=None, **kwargs): LINK_TYPE_CHOICES = [('none', _("No Link"))] LINK_TYPE_CHOICES.extend( t for t in getattr(LinkForm, 'LINK_TYPE_CHOICES') if t[0] != 'email') image_file = ModelChoiceField(queryset=Image.objects.all(), required=False, label=_("Image")) Form = type(str('ImageForm'), ( ImageFormMixin, getattr(LinkForm, 'get_form_class')(), ), { 'LINK_TYPE_CHOICES': LINK_TYPE_CHOICES, 'image_file': image_file }) kwargs.update(form=Form) return super(BootstrapImagePlugin, self).get_form(request, obj, **kwargs) def render(self, context, instance, placeholder): try: tags = get_image_tags(instance) except Exception as exc: logger.warning( "Unable generate image tags. Reason: {}".format(exc)) else: 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(BootstrapImagePlugin, cls).get_css_classes(obj) css_class = obj.glossary.get('css_class') if css_class: css_classes.append(css_class) return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapImagePlugin, cls).get_identifier(obj) try: content = force_text(obj.image) except AttributeError: content = _("No Image") return format_html('{0}{1}', identifier, content) @classmethod def sanitize_model(cls, obj): sanitized = False parent = obj.parent while parent.plugin_type != 'BootstrapColumnPlugin': parent = parent.parent if parent is None: logger.warning( "ImagePlugin(pk={}) has no ColumnPlugin as ancestor.". format(obj.pk)) return grid_column = parent.get_bound_plugin().get_grid_instance() min_max_bounds = grid_column.get_min_max_bounds() if obj.glossary.get('column_bounds') != min_max_bounds: obj.glossary['column_bounds'] = min_max_bounds sanitized = True obj.glossary.setdefault('media_queries', {}) for bp in Breakpoint: media_query = '{} {:.2f}px'.format(bp.media_query, grid_column.get_bound(bp).max) if obj.glossary['media_queries'].get(bp.name) != media_query: obj.glossary['media_queries'][bp.name] = media_query sanitized = True return sanitized
class BootstrapButtonMixin(object): require_parent = True parent_classes = ( 'BootstrapColumnPlugin', 'SimpleWrapperPlugin', ) render_template = 'cascade/bootstrap3/button.html' allow_children = False text_enabled = True default_css_class = 'btn' default_css_attributes = ( 'button-type', 'button-size', 'button-options', 'quick-float', ) glossary_fields = ( PartialFormField('button-type', ButtonTypeRenderer.get_widget(), label=_("Button Type"), initial='btn-default', help_text=_("Display Link using this Button Style")), PartialFormField('button-size', ButtonSizeRenderer.get_widget(), label=_("Button Size"), initial='', help_text=_("Display Link using this Button Size")), PartialFormField( 'button-options', widgets.CheckboxSelectMultiple(choices=( ('btn-block', _('Block level')), ('disabled', _('Disabled')), )), label=_("Button Options"), ), PartialFormField( 'quick-float', widgets.RadioSelect(choices=( ('', _("Do not float")), ('pull-left', _("Pull left")), ('pull-right', _("Pull right")), )), label=_("Quick Float"), initial='', help_text=_("Float the button to the left or right.")), PartialFormField( 'icon-left', GlyphiconRenderer.get_widget(), label=_("Prepend icon"), initial='', help_text=_("Prepend a Glyphicon before the content.")), PartialFormField('icon-right', GlyphiconRenderer.get_widget(), label=_("Append icon"), initial='', help_text=_("Append a Glyphicon after the content.")), ) def render(self, context, instance, placeholder): context = super(BootstrapButtonMixin, self).render(context, instance, placeholder) mini_template = '{0}<span class="glyphicon glyphicon-{1} {2}" aria-hidden="true"></span>{3}' icon_left = instance.glossary.get('icon-left') if icon_left: context['icon_left'] = format_html(mini_template, '', icon_left, 'cascade-icon-left', ' ') icon_right = instance.glossary.get('icon-right') if icon_right: context['icon_right'] = format_html(mini_template, ' ', icon_right, 'cascade-icon-right', '') return context
class BootstrapJumbotronPlugin(BootstrapPluginBase): name = _("Jumbotron") model_mixins = (ImagePropertyMixin, ImageBackgroundMixin) form = JumbotronPluginForm default_css_class = 'jumbotron' parent_classes = ['BootstrapColumnPlugin'] require_parent = False allow_children = True alien_child_classes = True raw_id_fields = ('image_file', ) fields = ( 'glossary', 'image_file', ) render_template = 'cascade/bootstrap3/jumbotron.html' ATTACHMENT_CHOICES = ('scroll', 'fixed', 'local') VERTICAL_POSITION_CHOICES = ('top', '10%', '20%', '30%', '40%', 'center', '60%', '70%', '80%', '90%', 'bottom') HORIZONTAL_POSITION_CHOICES = ('left', '10%', '20%', '30%', '40%', 'center', '60%', '70%', '80%', '90%', 'right') REPEAT_CHOICES = ('repeat', 'repeat-x', 'repeat-y', 'no-repeat') SIZE_CHOICES = ('auto', 'width/height', 'cover', 'contain') container_glossary_fields = ( GlossaryField( widgets.CheckboxSelectMultiple( choices=get_widget_choices(), renderer=ContainerBreakpointsRenderer), label=_("Available Breakpoints"), name='breakpoints', initial=list(BS3_BREAKPOINT_KEYS)[::-1], help_text=_( "Supported display widths for Bootstrap's grid system.")), GlossaryField( MultipleCascadingSizeWidget(BS3_BREAKPOINT_KEYS, allowed_units=['px', '%'], required=False), label=_("Adapt Picture Heights"), name='container_max_heights', initial={ 'xs': '100%', 'sm': '100%', 'md': '100%', 'lg': '100%' }, help_text= _("Heights of picture in percent or pixels for distinct Bootstrap's breakpoints." )), GlossaryField(widgets.CheckboxSelectMultiple( choices=BootstrapPicturePlugin.RESIZE_OPTIONS), label=_("Resize Options"), name='resize_options', initial=['crop', 'subject_location', 'high_resolution'], help_text=_("Options to use when resizing the image.")), ) background_color = GlossaryField( ColorPickerWidget(), label=_("Background color"), ) background_repeat = GlossaryField( widgets.RadioSelect(choices=[(c, c) for c in REPEAT_CHOICES]), initial='no-repeat', label=_("This property specifies how an image repeates."), ) background_attachment = GlossaryField( widgets.RadioSelect(choices=[(c, c) for c in ATTACHMENT_CHOICES]), initial='local', label= _("This property specifies how to move the background relative to the viewport." ), ) background_vertical_position = GlossaryField( widgets.Select(choices=[(c, c) for c in VERTICAL_POSITION_CHOICES]), initial='center', label= _("This property moves a background image vertically within its container." ), ) background_horizontal_position = GlossaryField( widgets.Select(choices=[(c, c) for c in HORIZONTAL_POSITION_CHOICES]), initial='center', label= _("This property moves a background image horizontally within its container." ), ) background_size = GlossaryField( widgets.RadioSelect(choices=[(c, c) for c in SIZE_CHOICES]), initial='auto', label=_("Background size"), help_text=_("This property specifies how an image is sized."), ) background_width_height = GlossaryField( MultipleCascadingSizeWidget(['width', 'height'], allowed_units=['px', '%'], required=False), label=_("Background width and height"), help_text= _("This property specifies the width and height of a background image." ), ) footnote_html = """ <p>For more information about the Jumbotron please read </p> """ class Media: css = {'all': (settings.CMSPLUGIN_CASCADE['fontawesome_css_url'], )} js = resolve_dependencies('cascade/js/admin/jumbotronplugin.js') def get_form(self, request, obj=None, **kwargs): if self.get_parent_instance(request) is None: # we only ask for breakpoints, if the jumbotron is the root of the placeholder kwargs.update(glossary_fields=list(self.container_glossary_fields)) kwargs['glossary_fields'].extend(self.glossary_fields) form = super(BootstrapJumbotronPlugin, self).get_form(request, obj, **kwargs) return form def render(self, context, instance, placeholder): # image shall be rendered in a responsive context using the ``<picture>`` element elements = get_picture_elements(context, instance) context.update({ 'instance': instance, 'placeholder': placeholder, 'elements': [e for e in elements if 'media' in e] if elements else [], }) return super(BootstrapJumbotronPlugin, self).render(context, instance, placeholder) @classmethod def sanitize_model(cls, obj): # if the jumbotron is the root of the placeholder, we consider it as "fluid" obj.glossary['fluid'] = obj.parent is None sanitized = super(BootstrapJumbotronPlugin, cls).sanitize_model(obj) compute_media_queries(obj) return sanitized @classmethod def get_identifier(cls, obj): identifier = super(BootstrapJumbotronPlugin, cls).get_identifier(obj) try: content = obj.image.name or obj.image.original_filename except AttributeError: content = _("Without background image") return format_html('{0}{1}', identifier, content)
class FilterableListForm(forms.Form): title = forms.CharField( max_length=250, required=False, widget=forms.TextInput(attrs={ 'id': 'o-filterable-list-controls_title', 'class': 'a-text-input a-text-input__full', 'placeholder': 'Search for a specific word in item title', }) ) from_date = FilterableDateField( field_id='o-filterable-list-controls_from-date' ) to_date = FilterableDateField( field_id='o-filterable-list-controls_to-date' ) categories = forms.MultipleChoiceField( required=False, choices=ref.page_type_choices, widget=widgets.CheckboxSelectMultiple() ) topics = forms.MultipleChoiceField( required=False, choices=[], widget=widgets.SelectMultiple(attrs={ 'id': 'o-filterable-list-controls_topics', 'class': 'o-multiselect', 'data-placeholder': 'Search for topics', 'multiple': 'multiple', }) ) authors = forms.MultipleChoiceField( required=False, choices=[], widget=widgets.SelectMultiple(attrs={ 'id': 'o-filterable-list-controls_authors', 'class': 'o-multiselect', 'data-placeholder': 'Search for authors', 'multiple': 'multiple', }) ) preferred_datetime_format = '%m/%d/%Y' def __init__(self, *args, **kwargs): self.filterable_pages = kwargs.pop('filterable_pages') self.wagtail_block = kwargs.pop('wagtail_block') super(FilterableListForm, self).__init__(*args, **kwargs) clean_categories(selected_categories=self.data.get('categories')) page_ids = self.filterable_pages.values_list('id', flat=True) self.set_topics(page_ids) self.set_authors(page_ids) def get_page_set(self): query = self.generate_query() return self.filterable_pages.filter(query).distinct().order_by( '-date_published' ) def first_page_date(self): first_post = self.filterable_pages.order_by('date_published').first() if first_post: return first_post.date_published else: return date(2010, 1, 1) def prepare_options(self, arr): """ Returns an ordered list of tuples of the format ('tag-slug-name', 'Tag Display Name') """ arr = Counter(arr).most_common() # Order by most to least common # Grab only the first tuple in the generated tuple, # which includes a count we do not need return [x[0] for x in arr] # Populate Topics' choices def set_topics(self, page_ids): if self.wagtail_block: self.fields['topics'].choices = self.wagtail_block.block \ .get_filterable_topics(page_ids, self.wagtail_block.value) # Populate Authors' choices def set_authors(self, page_ids): authors = Tag.objects.filter( v1_cfgovauthoredpages_items__content_object__id__in=page_ids ).values_list('slug', 'name') options = self.prepare_options(arr=authors) self.fields['authors'].choices = options def clean(self): cleaned_data = super(FilterableListForm, self).clean() if self.errors.get('from_date') or self.errors.get('to_date'): return cleaned_data else: ordered_dates = self.order_from_and_to_date_filters(cleaned_data) transformed_dates = self.set_interpreted_date_values(ordered_dates) return transformed_dates def order_from_and_to_date_filters(self, cleaned_data): from_date = cleaned_data.get('from_date') to_date = cleaned_data.get('to_date') # Check if both date_lte and date_gte are present. # If the 'start' date is after the 'end' date, swap them. if (from_date and to_date) and to_date < from_date: data = dict(self.data) data_to_date = data['to_date'] self.cleaned_data['to_date'], data['to_date'] = \ from_date, data['from_date'] self.cleaned_data['from_date'], data['from_date'] = \ to_date, data_to_date self.data = data return self.cleaned_data def set_interpreted_date_values(self, cleaned_data): from_date = cleaned_data.get('from_date') to_date = cleaned_data.get('to_date') # If from_ or to_ is filled in, fill them both with sensible values. # If neither is filled in, leave them both blank. if from_date or to_date: if from_date: self.data['from_date'] = date.strftime( cleaned_data['from_date'], self.preferred_datetime_format) else: # If there's a 'to_date' and no 'from_date', # use date of earliest possible filter result as 'from_date'. earliest_results = self.first_page_date() cleaned_data['from_date'] = earliest_results self.data['from_date'] = date.strftime( earliest_results, self.preferred_datetime_format) if to_date: transformed_to_date = end_of_time_period( self.data['to_date'], cleaned_data['to_date']) cleaned_data['to_date'] = transformed_to_date self.data['to_date'] = date.strftime( transformed_to_date, self.preferred_datetime_format) else: # If there's a 'from_date' but no 'to_date', use today's date. today = date.today() cleaned_data['to_date'] = today self.data['to_date'] = date.strftime( today, self.preferred_datetime_format) return cleaned_data # Sets the html name by replacing the render method to use the given name. def set_field_html_name(self, field, new_name): """ This creates wrapper around the normal widget rendering, allowing for a custom field name (new_name). """ old_render = field.widget.render field.widget.render = lambda name, value, **kwargs: \ old_render(new_name, value, **kwargs) # Generates a query by iterating over the zipped collection of # tuples. def generate_query(self): final_query = Q() if self.is_bound: for query, field_name in zip( self.get_query_strings(), self.declared_fields ): if self.cleaned_data.get(field_name): final_query &= \ Q((query, self.cleaned_data.get(field_name))) return final_query # Returns a list of query strings to associate for each field, ordered by # the field declaration for the form. Note: THEY MUST BE ORDERED IN THE # SAME WAY AS THEY ARE DECLARED IN THE FORM DEFINITION. def get_query_strings(self): return [ 'title__icontains', # title 'date_published__gte', # from_date 'date_published__lte', # to_date 'categories__name__in', # categories 'tags__slug__in', # topics 'authors__slug__in', # authors ]
class BootstrapPicturePlugin(ImageAnnotationMixin, LinkPluginBase): name = _("Picture") model_mixins = ( ImagePropertyMixin, LinkElementMixin, ) module = 'Bootstrap' parent_classes = ['BootstrapColumnPlugin', 'SimpleWrapperPlugin'] require_parent = True allow_children = False raw_id_fields = ('image_file', ) admin_preview = False ring_plugin = 'PicturePlugin' render_template = 'cascade/bootstrap4/linked-picture.html' default_css_class = 'img-fluid' default_css_attributes = ('image_shapes', ) html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'} html_tag_attributes.update(LinkPluginBase.html_tag_attributes) fields = ['image_file'] + list(LinkPluginBase.fields) RESIZE_OPTIONS = [('upscale', _("Upscale image")), ('crop', _("Crop image")), ('subject_location', _("With subject location")), ('high_resolution', _("Optimized for Retina"))] responsive_heights = GlossaryField( MultipleCascadingSizeWidget([bp.name for bp in Breakpoint], allowed_units=['px', '%'], required=False), label=_("Adapt Picture Heights"), initial={ 'xs': '100%', 'sm': '100%', 'md': '100%', 'lg': '100%', 'xl': '100%' }, help_text= _("Heights of picture in percent or pixels for distinct Bootstrap's breakpoints." ), ) responsive_zoom = GlossaryField( MultipleCascadingSizeWidget([bp.name for bp in Breakpoint], allowed_units=['%'], required=False), label=_("Adapt Picture Zoom"), initial={ 'xs': '0%', 'sm': '0%', 'md': '0%', 'lg': '0%', 'xl': '0%' }, help_text= _("Magnification of picture in percent for distinct Bootstrap's breakpoints." ), ) resize_options = GlossaryField( widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['subject_location', 'high_resolution']) class Media: js = ['cascade/js/admin/pictureplugin.js'] def get_form(self, request, obj=None, **kwargs): LINK_TYPE_CHOICES = [('none', _("No Link"))] LINK_TYPE_CHOICES.extend( t for t in getattr(LinkForm, 'LINK_TYPE_CHOICES') if t[0] != 'email') image_file = ModelChoiceField(queryset=Image.objects.all(), required=False, label=_("Image")) Form = type(str('ImageForm'), ( ImageFormMixin, getattr(LinkForm, 'get_form_class')(), ), { 'LINK_TYPE_CHOICES': LINK_TYPE_CHOICES, 'image_file': image_file }) kwargs.update(form=Form) return super(BootstrapPicturePlugin, self).get_form(request, obj, **kwargs) def render(self, context, instance, placeholder): # image shall be rendered in a responsive context using the picture element try: elements = get_picture_elements(instance) except Exception as exc: logger.warning( "Unable generate picture elements. Reason: {}".format(exc)) else: context.update({ 'instance': instance, 'is_fluid': True, 'placeholder': placeholder, 'elements': elements, }) return context @classmethod def get_css_classes(cls, obj): css_classes = cls.super(BootstrapPicturePlugin, cls).get_css_classes(obj) css_class = obj.glossary.get('css_class') if css_class: css_classes.append(css_class) return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapPicturePlugin, cls).get_identifier(obj) try: content = force_text(obj.image) except AttributeError: content = _("No Picture") return format_html('{0}{1}', identifier, content) @classmethod def sanitize_model(cls, obj): sanitized = False parent = obj.parent while parent.plugin_type != 'BootstrapColumnPlugin': parent = parent.parent if parent is None: logger.warning( "PicturePlugin(pk={}) has no ColumnPlugin as ancestor.". format(obj.pk)) return grid_column = parent.get_bound_plugin().get_grid_instance() obj.glossary.setdefault('media_queries', {}) for bp in Breakpoint: obj.glossary['media_queries'].setdefault(bp.name, {}) width = round(grid_column.get_bound(bp).max) if obj.glossary['media_queries'][bp.name].get('width') != width: obj.glossary['media_queries'][bp.name]['width'] = width sanitized = True if obj.glossary['media_queries'][bp.name].get( 'media') != bp.media_query: obj.glossary['media_queries'][ bp.name]['media'] = bp.media_query sanitized = True return sanitized
class BootstrapContainerPlugin(BootstrapPluginBase): name = _("Container") require_parent = False form = BootstrapContainerForm WIDGET_CHOICES = ( ('xs', _("Tiny (<{sm[0]}px)".format(**CASCADE_BREAKPOINTS_DICT))), ('sm', _("Small (≥{sm[0]}px and <{md[0]}px)".format(**CASCADE_BREAKPOINTS_DICT))), ('md', _("Medium (≥{md[0]}px and <{lg[0]}px)".format(**CASCADE_BREAKPOINTS_DICT))), ('lg', _("Large (≥{lg[0]}px)".format(**CASCADE_BREAKPOINTS_DICT))), ) glossary_fields = ( PartialFormField('breakpoints', widgets.CheckboxSelectMultiple(choices=WIDGET_CHOICES, renderer=ContainerBreakpointsRenderer), label=_('Available Breakpoints'), initial=['lg', 'md', 'sm', 'xs'], help_text=_("Supported display widths for Bootstrap's grid system.") ), PartialFormField('fluid', widgets.CheckboxInput(), label=_('Fluid Container'), initial=False, help_text=_("Changing your outermost '.container' to '.container-fluid'.") ), ) glossary_variables = ['container_max_widths', 'media_queries'] class Media: css = {'all': ('//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css',)} js = ['cascade/js/admin/containerplugin.js'] @classmethod def get_identifier(cls, obj): identifier = super(BootstrapContainerPlugin, cls).get_identifier(obj) breakpoints = obj.glossary.get('breakpoints') content = obj.glossary.get('fluid') and '(fluid) ' or '' if breakpoints: devices = ', '.join([force_text(CASCADE_BREAKPOINTS_DICT[bp][2]) for bp in breakpoints]) content = _("{0}for {1}").format(content, devices) return format_html('{0}{1}', identifier, content) @classmethod def get_css_classes(cls, obj): css_classes = super(BootstrapContainerPlugin, cls).get_css_classes(obj) if obj.glossary.get('fluid'): css_classes.append('container-fluid') else: css_classes.append('container') return css_classes def save_model(self, request, obj, form, change): super(BootstrapContainerPlugin, self).save_model(request, obj, form, change) obj.sanitize_children() @classmethod def sanitize_model(cls, obj): sanitized = super(BootstrapContainerPlugin, cls).sanitize_model(obj) parent_glossary = obj.get_parent_glossary() # compute the max width and the required media queries for each chosen breakpoint obj.glossary['container_max_widths'] = max_widths = {} obj.glossary['media_queries'] = media_queries = {} breakpoints = obj.glossary.get('breakpoints', []) last_index = len(breakpoints) - 1 for index, bp in enumerate(breakpoints): try: max_widths[bp] = parent_glossary['container_max_widths'][bp] except KeyError: max_widths[bp] = CASCADE_BREAKPOINTS_DICT[bp][3] if last_index > 0: if index == 0: next_bp = breakpoints[1] media_queries[bp] = ['(max-width: {0}px)'.format(CASCADE_BREAKPOINTS_DICT[next_bp][0])] elif index == last_index: media_queries[bp] = ['(min-width: {0}px)'.format(CASCADE_BREAKPOINTS_DICT[bp][0])] else: next_bp = breakpoints[index + 1] media_queries[bp] = ['(min-width: {0}px)'.format(CASCADE_BREAKPOINTS_DICT[bp][0]), '(max-width: {0}px)'.format(CASCADE_BREAKPOINTS_DICT[next_bp][0])] return sanitized
class BootstrapImagePlugin(ImageAnnotationMixin, LinkPluginBase): name = _("Image") model_mixins = ( ImagePropertyMixin, LinkElementMixin, ) module = 'Bootstrap' parent_classes = ('BootstrapColumnPlugin', ) require_parent = True allow_children = False raw_id_fields = ('image_file', ) admin_preview = False ring_plugin = 'ImagePlugin' render_template = 'cascade/bootstrap3/linked-image.html' default_css_attributes = ('image_shapes', ) html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'} fields = ('image_file', ) + LinkPluginBase.fields SHAPE_CHOICES = ( ('img-responsive', _("Responsive")), ('img-rounded', _('Rounded')), ('img-circle', _('Circle')), ('img-thumbnail', _('Thumbnail')), ) RESIZE_OPTIONS = ( ('upscale', _("Upscale image")), ('crop', _("Crop image")), ('subject_location', _("With subject location")), ('high_resolution', _("Optimized for Retina")), ) image_shapes = GlossaryField( widgets.CheckboxSelectMultiple(choices=SHAPE_CHOICES), label=_("Image Shapes"), initial=['img-responsive']) image_width_responsive = GlossaryField( CascadingSizeWidget(allowed_units=['%'], required=False), label=_("Responsive Image Width"), initial='100%', help_text=_( "Set the image width in percent relative to containing element."), ) image_width_fixed = GlossaryField( CascadingSizeWidget(allowed_units=['px'], required=False), label=_("Fixed Image Width"), help_text=_("Set a fixed image width in pixels."), ) image_height = GlossaryField( CascadingSizeWidget(allowed_units=['px', '%'], required=False), label=_("Adapt Image Height"), help_text= _("Set a fixed height in pixels, or percent relative to the image width." ), ) resize_options = GlossaryField( widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS), label=_("Resize Options"), help_text=_("Options to use when resizing the image."), initial=['subject_location', 'high_resolution']) class Media: js = ['cascade/js/admin/imageplugin.js'] def get_form(self, request, obj=None, **kwargs): utils.reduce_breakpoints(self, 'responsive_heights', request=request, obj=obj) image_file = ModelChoiceField(queryset=Image.objects.all(), required=False, label=_("Image")) Form = type( str('ImageForm'), ( ImageFormMixin, getattr(LinkForm, 'get_form_class')(), ), { 'LINK_TYPE_CHOICES': ImageFormMixin.LINK_TYPE_CHOICES, 'image_file': image_file }) kwargs.update(form=Form) return super(BootstrapImagePlugin, self).get_form(request, obj, **kwargs) def render(self, context, instance, placeholder): is_responsive = 'img-responsive' in instance.glossary.get( 'image_shapes', []) options = dict(instance.get_complete_glossary(), is_responsive=is_responsive) tags = utils.get_image_tags(context, instance, options) if 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(BootstrapImagePlugin, cls).get_css_classes(obj) css_class = obj.glossary.get('css_class') if css_class: css_classes.append(css_class) return css_classes @classmethod def get_identifier(cls, obj): identifier = super(BootstrapImagePlugin, cls).get_identifier(obj) try: content = force_text(obj.image) except AttributeError: content = _("No Image") return format_html('{0}{1}', identifier, content)
class ArticleForm(Form): title = fields.CharField( max_length=128, required=True, label='标题(必填,128个字符以内)', widget=widgets.TextInput(attrs={"class": "form-control"}), error_messages={ 'invalid': '标题长度过长', 'required': '标题不能为空' }) summary = fields.CharField(max_length=255, required=True, label="摘要(必填,文章摘要,255个字符以内)", widget=widgets.Textarea(attrs={ "class": "form-control", "rows": 3 }), error_messages={ 'invalid': '摘要长度过长', 'required': '摘要不能为空' }) content = fields.CharField(required=False, label="正文", widget=widgets.Textarea(attrs={ "class": "form-control kind-editor", "rows": 40 })) category_id = fields.ChoiceField(choices=[], required=False, label="文章分类", widget=widgets.RadioSelect(), error_messages={"invalid": "该分类不存在"}) tag_id = fields.MultipleChoiceField( choices=[], required=False, label="文章标签", widget=widgets.CheckboxSelectMultiple(), error_messages={"invalid": "该标签不存在"}) new_tag = fields.CharField( required=False, label="使用新的标签(多个关键字之间用英文“,”分隔,最多不超过10个)", widget=widgets.TextInput(attrs={"class": "form-control"}), ) webplate_id = fields.ChoiceField( choices=[], required=False, label="网站分类", widget=widgets.RadioSelect(), error_messages={"invalid": "该网站分类不存在"}, ) def __init__(self, request, *args, **kwargs): """ :param bid: bid为blog账户的id :param aid: aid为该blog下某篇文章的id,如果是添加文章aid=None, 如果是修改文章,aid不为None :param args: 其基类Form的位置参数 :param kwargs: 其基类Form的关键字参数 """ super(ArticleForm, self).__init__(*args, **kwargs) self.bid = request.logged_blog.nid self.fields[ 'webplate_id'].choices = web_models.WebPlate.objects.values_list( 'nid', 'title') self.fields[ 'category_id'].choices = web_models.Category.objects.filter( blog_id=self.bid).values_list('nid', 'title') self.fields['tag_id'].choices = web_models.Tag.objects.filter( blog_id=self.bid).values_list('nid', 'title') def clean_new_tag(self): """ new_tag是用户新增的tag,要求以逗号分隔,不能为空,检查是否和用户已有的标签tag_dic存在重复: 如果重复则不创建Tag类实例,但是将它添加到返回列表中 如果不重复,且不为空,创建Tag类实例,同时将它添加到返回列表中 :return: 返回列表,列表中的元素是需要和Article类实例创建对对多关系的。 """ data = self.cleaned_data.get("new_tag", "") new_tag_lst = data.strip().split(',') length = len(new_tag_lst) if not length: return [] elif length > 10: raise ValidationError('新增便签长度超过10个') try: new_tag_set = set([nt.strip() for nt in new_tag_lst]) tags = web_models.Tag.objects.filter(blog_id=self.bid).only( "title", 'nid') tag_dic = {tag.title: tag.nid for tag in tags} ret = [] for nt in new_tag_set: if nt in tag_dic: ret.append(tag_dic[nt]) elif nt and nt not in tag_dic: # 当标签不为空字符串,且数据库中没有该tag时,添加该tag t = web_models.Tag.objects.create(title=nt, blog_id=self.bid) ret.append(t.nid) return ret except: raise ValidationError("添加的新标签有误,请修改后重试")
class TeacherModelForm(StarkModelForm): birthday = Ffields.DateField( required=False, label='生日', widget=Fwidgets.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'style': 'width: 600px' })) identity = form_models.ModelChoiceField( initial=1, empty_label=None, label='职务', queryset=teamodels.Identity.objects.all(), widget=Fwidgets.RadioSelect()) course = form_models.ModelMultipleChoiceField( required=False, label='所带课程', queryset=scmodels.Course.objects.all(), widget=Fwidgets.CheckboxSelectMultiple()) gender = Ffields.ChoiceField(required=False, label='性别', choices=((1, '男'), (2, '女')), widget=Fwidgets.RadioSelect()) def __init__(self, *args, **kwargs): school_id = kwargs.pop('school_id') super(TeacherModelForm, self).__init__(*args, **kwargs) if school_id: self.school_id = school_id grade = form_models.ModelChoiceField( required=False, queryset=scmodels.Grade.objects.filter( stuclass__school_id=school_id).distinct(), widget=Fwidgets.Select(attrs={ 'class': 'form-control', 'id': 'grade' }), empty_label=None) self.fields['grade'] = grade class Meta: model = teamodels.TeacherInfo fields = ('last_name', 'first_name', 'gender', 'birthday', 'telephone', 'wechat', 'identity', 'course') widgets = { "last_name": Fwidgets.TextInput(attrs={ 'class': 'form-control', 'style': 'width: 600px' }), "first_name": Fwidgets.TextInput(attrs={ 'class': 'form-control', 'style': 'width: 600px' }), 'wechat': Fwidgets.TextInput(attrs={ 'class': 'form-control', 'style': 'width: 600px' }), 'telephone': Fwidgets.TextInput(attrs={ 'class': 'form-control', 'style': 'width: 600px' }), } error_messages = { "last_name": { "required": "请输入老师姓" }, "first_name": { "required": "请输入老师名" }, "gender": { "required": "请选择性别" }, } labels = { 'last_name': '老师姓(必填)', 'first_name': '老师名(必填)', } def clean(self): ''' 校验该教师是否已经存在 :return: ''' birthday = self.cleaned_data.get('birthday') last_name = self.cleaned_data.get('last_name') first_name = self.cleaned_data.get('first_name') teacher_obj = teamodels.TeacherInfo.objects.filter( birthday=birthday, first_name=first_name, last_name=last_name, school=self.school_id) if teacher_obj: raise ValidationError('该教师已存在') return self.cleaned_data