def test_should_render_correct_markup_for_entities_without_image(self): p1 = Page.objects.create(title='A', slug='a') p2 = Page.objects.create(title='B', slug='b') field = ModelCollectionField(queryset=Page.objects.all()) html = field.widget.render('fieldname', [p1.pk, p2.pk, 999]) self.assertIn('<div class="cubane-listing-item collection-item cubane-listing-grid-item" title="A" data-id="%s">' % p1.pk, html) self.assertIn('<div class="cubane-listing-item collection-item cubane-listing-grid-item" title="B" data-id="%s">' % p2.pk, html) p1.delete() p2.delete()
def test_should_render_correct_markup_for_entities_with_image(self): m = Media.objects.create(caption='A Picture', filename='a.jpg', is_image=True, width=512, height=512) p1 = Page.objects.create(title='A', slug='a', image=m) p2 = Page.objects.create(title='B', slug='b', image=m) try: field = ModelCollectionField(queryset=Page.objects.all()) html = field.widget.render('fieldname', [p1.pk, p2.pk, 999]) self.assertIn('value="%s"' % p1.pk, html) self.assertIn('value="%s"' % p2.pk, html) self.assertNotIn('value="%s"' % 999, html) self.assertIn('data-background-image data-shape="original" data-path="/0/%d/a.jpg"' % m.pk, html) finally: p1.delete() p2.delete()
class ProductFormBase(BaseModelForm): class Meta: model = get_product_model() exclude = ['seq', 'varieties', 'delivery_options', '_related_products'] widgets = { 'title': forms.TextInput(attrs={ 'class': 'slugify', 'autocomplete': 'off' }), 'slug': forms.TextInput(attrs={ 'class': 'slug', 'autocomplete': 'off' }), 'price': BootstrapTextInput(prepend=settings.CURRENCY, attrs={'class': 'input-medium'}), 'deposit': BootstrapTextInput(prepend=settings.CURRENCY, attrs={'class': 'input-medium'}), 'rrp': BootstrapTextInput(prepend=settings.CURRENCY, attrs={'class': 'input-medium'}), 'previous_price': BootstrapTextInput(prepend=settings.CURRENCY, attrs={'class': 'input-medium'}), 'finance_options': forms.CheckboxSelectMultiple() } tabs = [{ 'title': 'Title', 'fields': [ 'title', 'slug', 'category', 'categories', 'legacy_url', 'excerpt', '_meta_title', '_meta_description', '_meta_keywords', '_meta_preview' ] }, { 'title': 'Content', 'fields': ['_excerpt', 'description'] }, { 'title': 'Price and Availability', 'fields': [ 'rrp', 'previous_price', 'price', 'draft', 'non_returnable', 'collection_only', 'pre_order', 'deposit', 'loan_exempt', 'finance_options', 'exempt_from_free_delivery', 'exempt_from_discount', 'feed_google', 'feed_amazon', 'stock', 'stocklevel', 'sku_enabled', 'sku', 'barcode_system', 'barcode', 'part_number', ] }, { 'title': 'Gallery', 'fields': ['image', '_gallery_images'] }, { 'title': 'Related Products', 'fields': ['_related_products_collection'] }, { 'title': 'SKU / Inventory', 'fields': ['_inventory'] }] sections = { 'title': 'Product Data', '_excerpt': 'Excerpt', '_meta_title': 'Meta Data', 'barcode_system': 'Identification', 'stock': 'Stock', 'sku_enabled': 'SKU / Inventory', '_meta_preview': 'Search Result Preview', 'rrp': 'Price', 'draft': 'Options', 'pre_order': 'Pre-order', 'loan_exempt': 'Finance', 'exempt_from_free_delivery': 'Exemption', 'feed_google': 'Channel Feeds', 'image': 'Product Images' } category = BrowseCategoryField( required=True, help_text='The category this product is listed under.') _meta_preview = fields.Field( label=None, required=False, help_text='This preview is for demonstration purposes only ' +\ 'and the actual search result may differ from the preview.', ) image = BrowseImagesField( required=False, help_text= 'Choose the main image for this product that is used on the product listing page.' ) _gallery_images = GalleryField( label='Image Gallery', required=False, queryset=Media.objects.filter(is_image=True), help_text= 'Add an arbitrarily number of images that are presented on the product details page.' ) _related_products_collection = ModelCollectionField( label='Related Products', required=False, queryset=get_product_model().objects.all(), url='/admin/products/', title='Products', model_title='Products', help_text= 'Add an arbitrarily number of related products to this product.') categories = ModelCollectionField( label='Categories', add_label='Add Category', required=True, queryset=get_category_model().objects.all(), url='/admin/categories/', title='Categories', model_title='Categories', viewmode=ModelCollectionField.VIEWMODE_LIST, allow_duplicates=False, sortable=False, help_text= 'Add an arbitrarily number of categories this product is listed under.' ) _inventory = RelatedListingField(view=InventoryView()) def configure(self, request, instance, edit): super(ProductFormBase, self).configure(request, instance, edit) # meta preview control self.fields['_meta_preview'].widget = MetaPreviewWidget( attrs={ 'class': 'no-label', 'path': request.path_info, 'form': self }) # excerpt self.fields[ '_excerpt'].help_text = 'Provide your elevator pitch to the customer (max. %d characters)' % settings.CMS_EXCERPT_LENGTH if request.settings.barcode_system and request.settings.sku_is_barcode: self.remove_field('sku') self.fields['barcode'].label = 'SKU / Barcode' self.fields[ 'barcode'].help_text = 'SKU / Barcode (%s)' % request.settings.barcode_system.upper( ) # multiple categories if settings.SHOP_MULTIPLE_CATEGORIES: self.remove_field('category') else: self.remove_field('categories') # loan applications if settings.SHOP_LOAN_ENABLED: queryset = FinanceOption.objects.filter( enabled=True, per_product=True).order_by('seq') self.fields['finance_options'].queryset = queryset if queryset.count() == 0: self.remove_field('finance_options') self.update_sections() else: self.remove_field('finance_options') self.remove_field('loan_exempt') # SKU / inventory if not (instance and instance.sku_enabled): self.remove_tab('SKU / Inventory') self.update_sections() def clean_slug(self): slug = self.cleaned_data.get('slug') if slug: products = get_product_model().objects.filter(slug=slug) if self._edit and self._instance: products = products.exclude(pk=self._instance.pk) if products.count() > 0: raise forms.ValidationError( 'This slug is already used. Please choose a different slug.' ) return slug def clean_excerpt(self): excerpt = self.cleaned_data.get('_excerpt') if excerpt: if len(excerpt) > settings.CMS_EXCERPT_LENGTH: raise forms.ValidationError( 'The maximum allowed length is %d characters.' % settings.CMS_EXCERPT_LENGTH) return excerpt def clean_price(self): return clean_price(self, 'price', self.cleaned_data) def clean_rrp(self): return clean_price(self, 'rrp', self.cleaned_data) def clean_previous_price(self): return clean_price(self, 'previous_price', self.cleaned_data) def clean(self): d = super(ProductFormBase, self).clean() barcode_system = d.get('barcode_system') barcode = d.get('barcode') if barcode_system is None: barcode_system = self._request.settings.barcode_system # verify that the barcode is correct... if barcode and barcode_system: try: d['barcode'] = verify_barcode(barcode_system, barcode) except BarcodeError, e: self.field_error('barcode', e.msg) return d
class InventoryForm(BaseModelForm, ProductSKUFormMixin): class Meta: model = ProductSKU fields = '__all__' widgets = { 'price': BootstrapTextInput(prepend=settings.CURRENCY, attrs={'class': 'input-medium'}), } sections = {'enabled': 'SKU', 'price': 'Price and Stock Level'} product = BrowseProductField() variety_options = ModelCollectionField( label='Variety Options', add_label='Add Variety Option', required=True, queryset=VarietyOption.objects.all(), url='/admin/variety-options/', title='Variety Options', model_title='Variety Option', viewmode=ModelCollectionField.VIEWMODE_LIST, allow_duplicates=False, sortable=False, help_text= 'Add the number of unique variety options that this Stock Keeping Unit (SKU) represents.' ) def configure(self, request, instance, edit): super(InventoryForm, self).configure(request, instance, edit) # barcode system try: product = instance.product except: product = None self._barcode_system = request.settings.get_barcode_system(product) if self._barcode_system: if request.settings.sku_is_barcode: self.remove_field('sku') self.fields['barcode'].label = 'SKU / Barcode' self.fields[ 'barcode'].help_text = 'SKU / Barcode (%s)' % self._barcode_system.upper( ) else: self.fields[ 'barcode'].help_text = 'Barcode (%s)' % self._barcode_system.upper( ) else: self.remove_field('barcode') def clean(self): """ Verify that SKU is unique and valid. """ d = super(InventoryForm, self).clean() product_model = get_product_model() # SKU sku = d.get('sku') if sku: # SKU must be unique across all product SKUs x = ProductSKU.objects.filter(sku=sku) if self._edit: x = x.exclude(pk=self._instance.pk) if x.count() > 0: self.field_error('sku', 'This SKU number already exists.') # SKU cannot match any product-specific SKU products = product_model.objects.filter(sku=sku) if products.count() > 0: self.field_error( 'sku', 'This SKU number already exists for product: %s' % products[0]) # barcode barcode = d.get('barcode') if barcode: # barcode must be unique across all product SKUs x = ProductSKU.objects.filter(barcode=barcode) if self._edit: x = x.exclude(pk=self._instance.pk) if x.count() > 0: self.field_error('barcode', 'This barcode number already exists.') # barcode cannot match any product-specific barcode products = product_model.objects.filter(barcode=barcode) if products.count() > 0: self.field_error( 'barcode', 'This barcode number already exists for product: %s' % products[0]) product = d.get('product') if product: # product must be SKU enabled if not product.sku_enabled: self.field_error( 'product', 'This product is not enabled for SKU numbers.') variety_options = d.get('variety_options') if variety_options: # all varieties must be enabled for SKU usage for variety_option in variety_options: if not variety_option.variety.sku: self.field_error( 'variety_options', 'The variety \'%s\' is not enabled for SKU numbers (\'%s\').' % (variety_option.variety, variety_option)) # all variety options must have a different variety. We cannot # map to multiple options of the same variety... variety_ids = [option.variety.pk for option in variety_options] if len(set(variety_ids)) != len(variety_ids): self.field_error( 'variety_options', 'A SKU number must be a unique combination of varieties and cannot map to multiple options of the same variety.' ) # combination of variety options must match all required # varieties x = ProductSKU.objects.filter(product=product, enabled=True) if self._edit: x = x.exclude(pk=self._instance.pk) if x.count() > 0: # all required varieties must be assigned... required_varieties = [ variety_option.variety for variety_option in x[0].variety_options.all() ] required_variety_ids = [v.pk for v in required_varieties] varieties = [option.variety for option in variety_options] variety_ids = [v.pk for v in varieties] for required_variety in required_varieties: if required_variety.pk not in variety_ids: self.field_error( 'variety_options', 'This SKU number must map to the required variety: \'%s\'.' % (required_variety)) # we cannot have a variety that is not allowed... for variety_option in variety_options: if variety_option.variety.pk not in required_variety_ids: self.field_error( 'variety_options', 'Variety option \'%s\' does not belong to \'%s\'.' % (variety_option, ' or '.join( ['%s' % v for v in required_varieties]))) # combination of variety options must be unique for this product x = ProductSKU.objects.filter(product=product, enabled=True) for variety_option in variety_options: x = x.filter(variety_options=variety_option) if self._edit: x = x.exclude(pk=self._instance.pk) if x.count() > 0: self.field_error( 'variety_options', 'This combination of variety options already exists.') return d
class PageFormBase(BaseModelForm): """ Form for editing CMS pages. This is the base form class for cms pages and cms entities. However, when deriving new forms, use PageForm or EntityForm. """ class Meta: exclude = ['_nav', '_data'] widgets = { 'title': forms.TextInput(attrs={ 'class': 'slugify', 'autocomplete': 'off' }), 'slug': forms.TextInput(attrs={ 'class': 'slug', 'autocomplete': 'off' }), 'seq': NumberInput(attrs={ 'class': 'input-mini', 'min': '0' }), '_excerpt': widgets.Textarea(attrs={'rows': '8'}), '_meta_description': widgets.Textarea() } tabs = [{ 'title': 'Content', 'fields': [] }, { 'title': 'Title', 'fields': [ 'title', 'slug', 'legacy_url', '_excerpt', '_meta_title', '_meta_description', '_meta_keywords', '_meta_preview', ] }, { 'title': 'Presentation', 'fields': [ 'template', ] }, { 'title': 'Gallery', 'fields': ['image', '_gallery_images'] }, { 'title': 'Visibility', 'fields': ['disabled', 'sitemap', 'seq'] }] sections = { 'title': 'Page Data', '_excerpt': 'Excerpt', '_meta_title': 'Meta Data', '_meta_preview': 'Search Result Preview', 'template': 'Template', 'image': 'Primary Image and Gallery' } limits = { '_meta_title': FormInputLimit(65), '_meta_description': FormInputLimit(240) } image = BrowseImagesField(required=False) _gallery_images = ModelCollectionField( label='Image Gallery', required=False, queryset=Media.objects.filter(is_image=True), url='/admin/images/', title='Gallery', model_title='Images', help_text='Add an arbitrarily number of images to this page.') _meta_preview = fields.Field( label=None, required=False, help_text='This preview is for demonstration purposes only ' + \ 'and the actual search result may differ from the preview.' ) parent = BrowsePagesField( required=False, help_text='Select the parent page for this page for the purpose of ' + \ 'presenting multi-level navigation.' ) def clean_legacy_url(self): """ Allow a full domain name to be copy into the legacy_url field and have the absolute path extracted automatically. """ legacy_url = self.cleaned_data.get('legacy_url') if legacy_url: legacy_url = to_legacy_url(legacy_url) return legacy_url def clean(self): """ Detect navigation changes """ cleaned_data = super(PageFormBase, self).clean() if self._instance and self._instance.pk: nav_changed = (self.cleaned_data.get('title') != getattr( self._instance, 'title', None) or self.cleaned_data.get('navigation_title') != getattr(self._instance, 'navigation_title', None) or self.cleaned_data.get('slug') != getattr( self._instance, 'slug', None)) if nav_changed: self._instance.nav_updated_on = datetime.now() return cleaned_data def configure(self, request, instance, edit): """ Configure form """ super(PageFormBase, self).configure(request, instance, edit) from cubane.cms.templatetags.cms_tags import rewrite_image_references # create textarea field for each slot. if self.is_tabbed: for slotname in settings.CMS_SLOTNAMES: fieldname = 'slot_%s' % slotname self.fields[fieldname] = forms.CharField( required=False, widget=forms.Textarea( attrs={ 'class': 'editable-html preview no-label full-height', 'data-slotname': slotname })) self._tabs[0]['fields'].append(fieldname) # load initial content for each slot. if edit or self.is_duplicate: for slotname in settings.CMS_SLOTNAMES: fieldname = 'slot_%s' % slotname self.fields[fieldname].initial = \ rewrite_image_references(instance.get_slot_content(slotname)) self.fields['_meta_preview'].widget = MetaPreviewWidget( attrs={ 'class': 'no-label', 'path': request.path_info, 'form': self })
class TestModelCollectionFieldForm(BaseForm): pages = ModelCollectionField(queryset=Page.objects.all())