Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

We've moved the forum!

Please use forum.silverstripe.org for any new questions (announcement).
The forum archive will stick around, but will be read only.

You can also use our Slack channel or StackOverflow to ask for help.
Check out our community overview for more options to contribute.

Customising the CMS /

Moderators: martimiz, Sean, Ed, biapar, Willr, Ingo, swaiba

New tabs but no fields in CMS??


Go to End


10 Posts   4446 Views

Avatar
mhdesign

Community Member, 216 Posts

23 October 2014 at 9:54am

A quickie -- I hope! I've extended CMS to add two new editable fields to my page. I've got the tabs in the CMS but no fields?? Anybody see where I went wrong?

<?php

class ProductExtension extends DataExtension{
	
	private static $db = array(
		'Summary' => 'HTMLText',
		'Facts' => 'HTMLText'
	);

	public function updateCMSFields(FieldList $fields) {
		$fields->addFieldToTab("Root.Summary",
			new HTMLEditorField("Summary")
		);
		
		$fields->addFieldToTab("Root.Facts",
			new HTMLEditorField("Facts")
		);
		
	}

}

Have taken screenshot of result. Please help!!

Attached Files
Avatar
mhdesign

Community Member, 216 Posts

24 October 2014 at 9:45am

Edited: 24/10/2014 11:19am

Not only that -- now I've discovered that the 'Save' and 'Publish' buttons on this pagetype has disappeared along with the bar across the bottom of the page (not other page types)!! Can somebody help please? What have I done wrong?

Looked for stray spaces, etc in my php to no avail!! Don't know what's happening!

Avatar
Nivanka

Community Member, 400 Posts

25 October 2014 at 3:42am

your code looks fine.

If you provide more information on what version of SilverStripe you are using etc, it will easier for someone to else.

also try flushing the template cache. just call a front end url with ?flush=1

Avatar
mhdesign

Community Member, 216 Posts

25 October 2014 at 2:28pm

Hay Nivanka, thanks for some feedback! I was wondering if anybody was 'out there'!

Good point about Silverstripe version -- 3.1.6 (latest build). Yes, I have done a '?flush=1'. I've also done lost of '?flush=all' and some '/dev/builds/' as well! It's got me beat! Only other thing I can think of is if I specifically need to do this in /admin/ or the root of the site?

Nothing else I would need to add anywhere (other than the corresponding field in m templates, obviously)?

Avatar
mhdesign

Community Member, 216 Posts

28 October 2014 at 2:19pm

Edited: 28/10/2014 2:20pm

Made some progress... it seems that a FlatTaxModifier file that I had updated by commenting out the lines that added a flat tax to an e-commerce total was causing the text entry fields (indicated above) to disappear. Unfortunately I still have no 'save/publish' bar on these pages. There is 'something rotten' in the state of Silverstripe... has anybody ever seen this??

If you have and you managed to fix it I'd love to know how -- PLEASE SPEAK NOW!!

Avatar
Nivanka

Community Member, 400 Posts

28 October 2014 at 2:51pm

can you post the PHP file here, so i can take a look.

Avatar
mhdesign

Community Member, 216 Posts

28 October 2014 at 3:04pm

Hi Nivanka, PHP code attached as requested. Thanks for your comments!

product.php allows users to add a product to the 'shop' area of the site.

<?php
/**
 * This is a standard Product page-type with fields like
 * Price, Weight, Model and basic management of
 * groups.
 *
 * It also has an associated Product_OrderItem class,
 * an extension of OrderItem, which is the mechanism
 * that links this page type class to the rest of the
 * eCommerce platform. This means you can add an instance
 * of this page type to the shopping cart.
 *
 * @package shop
 */
class Product extends Page implements Buyable {
	
	
	//private static $db = array(
	//				  'SummaryHTML' => 'HTMLText' //Summary tab for 'all products' page
   //);
	
	private static $db = array(
		'InternalItemID' => 'Varchar(30)', //ie SKU, ProductID etc (internal / existing recognition of product)
		'Model' => 'Varchar(30)',

		'CostPrice' => 'Currency', // Wholesale cost of the product to the merchant
		'BasePrice' => 'Currency', // Base retail price the item is marked at.

		//physical properties
		'Weight' => 'Decimal(9,2)',
		'Height' => 'Decimal(9,2)',
		'Width' => 'Decimal(9,2)',
		'Depth' => 'Decimal(9,2)',

		'Featured' => 'Boolean',
		'AllowPurchase' => 'Boolean',

		'Popularity' => 'Float' //storage for ClaculateProductPopularity task
	);

	private static $has_one = array(
		'Image' => 'Image'
	);

	private static $many_many = array(
		'ProductCategories' => 'ProductCategory'
	);

	private static $defaults = array(
		'AllowPurchase' => true,
		'ShowInMenus' => false
	);

	private static $casting = array(
		'Price' => 'Currency'
	);

	private static $summary_fields = array(
		'InternalItemID','Title','BasePrice'
	);

	private static $searchable_fields = array(
		'InternalItemID','Title','BasePrice','Featured'
	);

	private static $field_labels = array(
		'InternalItemID' => 'SKU',
		'Title' => 'Title',
		'BasePrice' => 'Price'
	);

	private static $singular_name = "Product";
	private static $plural_name = "Products";
	private static $icon = 'shop/images/icons/package';
	private static $default_parent = 'ProductCategory';
	private static $default_sort = '"Title" ASC';

	private static $global_allow_purchase = true;
	private static $allow_zero_price = false;
	private static $order_item = "Product_OrderItem";
	private static $min_opengraph_img_size = 0;

	private static $indexes = array(
		'Featured' => true,
		'AllowPurchase' => true,
		'InternalItemID' => true,
	);

	/**
	 * Add product fields to CMS
	 * @return FieldList updated field list
	 */
	public function getCMSFields() {
		self::disableCMSFieldsExtensions();
		$fields = parent::getCMSFields();
		$fields->fieldByName('Root.Main.Title')
			->setTitle(_t('Product.PAGETITLE', 'Product Title'));
			
		//general fields
		$fields->addFieldsToTab('Root.Main', array(
			TextField::create('InternalItemID', _t('Product.CODE', 'Product Code/SKU'), '', 30),
			DropdownField::create('ParentID', _t("Product.CATEGORY", "Category"), $this->categoryoptions())
				->setDescription(_t("Product.CATEGORYDESCRIPTION", "This is the parent page or default category.")),
			ListBoxField::create('ProductCategories', _t("Product.ADDITIONALCATEGORIES", "Additional Categories"),
					ProductCategory::get()
						->filter("ID:not", $this->getAncestors()->map('ID', 'ID'))
						->map('ID', 'NestedTitle')->toArray()
				)->setMultiple(true),
			TextField::create('Model', _t('Product.MODEL', 'Model'), '', 30),
			CheckboxField::create('Featured', _t('Product.FEATURED', 'Featured Product')),
			CheckboxField::create('AllowPurchase', _t('Product.ALLOWPURCHASE', 'Allow product to be purchased'), 1)
		), 'Content');
		
		//summary 
		//$fields -> addFieldToTab("Root.Summary",new HTMLEditorField("SummaryHTML"));
		
		//pricing
		$fields->addFieldsToTab('Root.Pricing', array(
			TextField::create('BasePrice', _t('Product.PRICE', 'Price'))
				->setDescription(_t('Product.PRICEDESC', "Base price to sell this product at."))
				->setMaxLength(12),
			TextField::create('CostPrice', _t('Product.COSTPRICE', 'Cost Price'))
				->setDescription(_t('Product.COSTPRICEDESC', 'Wholesale price before markup.'))
				->setMaxLength(12)
		));
		
		//physical measurements
		$weightunit = "kg"; //TODO: globalise / make custom
		$lengthunit = "cm";  //TODO: globalise / make custom
		$fields->addFieldsToTab('Root.Shipping', array(
			TextField::create('Weight', sprintf(_t('Product.WEIGHT', 'Weight (%s)'), $weightunit), '', 12),
			TextField::create('Height', sprintf(_t('Product.HEIGHT', 'Height (%s)'), $lengthunit), '', 12),
			TextField::create('Width', sprintf(_t('Product.WIDTH', 'Width (%s)'), $lengthunit), '', 12),
			TextField::create('Depth', sprintf(_t('Product.DEPTH', 'Depth (%s)'), $lengthunit), '', 12),
		));
		if(!$fields->dataFieldByName('Image')) {
			$fields->addFieldToTab('Root.Images',
				UploadField::create('Image', _t('Product.IMAGE', 'Product Image'))
			);
		}
		self::enableCMSFieldsExtensions();
		$this->extend('updateCMSFields', $fields);

		return $fields;
	}

	/**
	 * Helper function for generating list of categories to select from.
	 * @return array categories
	 */
	private function categoryoptions() {
		$categories = ProductCategory::get()->map('ID', 'NestedTitle')->toArray();
		$categories = array(
			0 => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page")
		) + $categories;
		if($this->ParentID && !($this->Parent() instanceof ProductCategory)){
			$categories = array(
				$this->ParentID => $this->Parent()->Title." (".$this->Parent()->i18n_singular_name().")"
			) + $categories;
		}

		return $categories;
	}

	/**
	 * Get ids of all categories that this product appears in.
	 * @return array ids list
	 */
	public function getCategoryIDs() {
		$ids = array();
		//ancestors
		foreach($this->getAncestors() as $ancestor){
			$ids[$ancestor->ID] = $ancestor->ID;
		}
		//additional categories
		$ids += $this->ProductCategories()->getIDList();

		return $ids;
	}

	/**
	 * Get all categories that this product appears in.
	 * @return DataList category data list
	 */
	public function getCategories(){
		return  ProductCategory::get()->byIDs($this->getCategoryIDs());
	}

	/**
	 * Conditions for whether a product can be purchased:
	 *  - global allow purchase is enabled
	 *  - product AllowPurchase field is true
	 *  - if variations, then one of them needs to be purchasable
	 *  - if not variations, selling price must be above 0
	 *
	 * Other conditions may be added by decorating with the canPurcahse function
	 *
	 * @param Member $member
	 * @param int $quantity
	 *
	 * @throws ShopBuyableException
	 *
	 * @return boolean
	 */
	public function canPurchase($member = null, $quantity = 1) {
		$global = self::config()->global_allow_purchase;
		if(!$global || !$this->AllowPurchase) {
			return false;
		}
		$allowpurchase = false;
		$extension = self::has_extension("ProductVariationsExtension");
		if($extension &&
			ProductVariation::get()->filter("ProductID",$this->ID)->first()
		) {
			foreach($this->Variations() as $variation) {
				try {
					if($variation->canPurchase($member, $quantity)) {
						$allowpurchase = true;
					
						break;
					}
				} catch(ShopBuyableException $e) {
				}
			}

			// if not allowed to buy after any variations then raise the last
			// exception again
			if(!$allowpurchase && isset($e)) {
				throw $e;

				return false;
			}
		} else if($this->sellingPrice() > 0 || self::config()->allow_zero_price) {
			$allowpurchase = true;
		}
		// Standard mechanism for accepting permission changes from decorators
		$extended = $this->extendedCan('canPurchase', $member, $quantity);
		if($allowpurchase && $extended !== null) {
			$allowpurchase = $extended;
		}

		return $allowpurchase;
	}

	/**
	 * Returns if the product is already in the shopping cart.
	 * @return boolean
	 */
	public function IsInCart() {
		$item = $this->Item();
		return $item && $item->exists() && $item->Quantity > 0;
	}

	/**
	 * Returns the order item which contains the product
	 * @return  OrderItem
	 */
	public function Item() {
		$filter = array();
		$this->extend('updateItemFilter', $filter);
		$item = ShoppingCart::singleton()->get($this, $filter);
		if(!$item){
			//return dummy item so that we can still make use of Item
			$item = $this->createItem();
		}
		$this->extend('updateDummyItem', $item);
		return $item;
	}

	/**
	 * @see Buyable::createItem()
	 */
	public function createItem($quantity = 1, $filter = null) {
		$orderitem = self::config()->order_item;
		$item = new $orderitem();
		$item->ProductID = $this->ID;
		if($filter){
			//TODO: make this a bit safer, perhaps intersect with allowed fields
			$item->update($filter);
		}
		$item->Quantity = $quantity;
		return $item;
	}

	/**
	 * The raw retail price the visitor will get when they
	 * add to cart. Can include discounts or markups on the base price.
	 */
	public function sellingPrice() {
		$price = $this->BasePrice;
		//TODO: this is not ideal, because prices manipulations will not happen in a known order
		$this->extend("updateSellingPrice", $price);
		//prevent negative values
		$price = $price < 0 ? 0 : $price;

		return $price;
	}

	/**
	 * This value is cased to Currency in temlates.
	 */
	public function getPrice() {
		return $this->sellingPrice();
	}

	public function setPrice($price) {
		$price = $price < 0 ? 0 : $price;
		$this->setField("BasePrice", $price);
	}

	/**
	 * Allow orphaned products to be viewed.
	 */
	public function isOrphaned() {
		return false;
	}

	public function Link() {
		$link = parent::Link();
		$this->extend('updateLink', $link);
		return $link;
	}

	/**
	 * If the product does not have an image, and a default image
	 * is defined in SiteConfig, return that instead.
	 * @return Image
	 */
	public function Image() {
		$image = $this->getComponent('Image');
		if ($image && $image->exists() && file_exists($image->getFullPath())) return $image;
		$image = SiteConfig::current_site_config()->DefaultProductImage();
		if ($image && $image->exists() && file_exists($image->getFullPath())) return $image;
		return $this->model->Image->newObject();
	}

    /**
     * Integration with opengraph module
     * @see https://github.com/tractorcow/silverstripe-opengraph
     * @return string opengraph type
     */
	public function getOGType() {
		return 'product';
	}

	/**
	 * Integration with the opengraph module
	 * @return string url of product image
	 */
	public function getOGImage() {
		if($image = $this->Image()){
			$min = self::config()->min_opengraph_img_size;
			$image = $min && $image->getWidth() < $min ? $image->setWidth($min) : $image;

			return Director::absoluteURL($image->URL);
		}
    }

	/**
	 * Link to add this product to cart.
	 * @return string link
	 */
	public function addLink() {
		return ShoppingCart_Controller::add_item_link($this);
	}

	/**
	 * Link to remove one of this product from cart.
	 * @return string link
	 */
	public function removeLink() {
		return ShoppingCart_Controller::remove_item_link($this);
	}

	/**
	 * Link to remove all of this product from cart.
	 * @return string link
	 */
	public function removeallLink() {
		return ShoppingCart_Controller::remove_all_item_link($this);
	}

}

class Product_Controller extends Page_Controller {

	private static $allowed_actions = array(
		'Form',
		'AddProductForm'
	);

	public $formclass = "AddProductForm"; //allow overriding the type of form used

	public function Form() {
		$formclass = $this->formclass;
		$form = new $formclass($this,"Form");
		$this->extend('updateForm', $form);
		return $form;
	}

}

class Product_OrderItem extends OrderItem {

	private static $db = array(
		'ProductVersion' => 'Int'
	);

	private static $has_one = array(
		'Product' => 'Product'
	);

	/**
	 * the has_one join field to identify the buyable
	 */
	private static $buyable_relationship = "Product";

	/**
	 * Get related product
	 *  - live version if in cart, or
	 *  - historical version if order is placed
	 *
	 * @param boolean $forcecurrent - force getting latest version of the product.
	 * @return Product
	 */
	public function Product($forcecurrent = false) {
		//TODO: this might need some unit testing to make sure it compliles with comment description
			//ie use live if in cart (however I see no logic for checking cart status)
		if($this->ProductID && $this->ProductVersion && !$forcecurrent){
			return Versioned::get_version('Product', $this->ProductID, $this->ProductVersion);
		}elseif(
			$this->ProductID && 
			$product = Versioned::get_one_by_stage("Product", "Live",
				"\"Product\".\"ID\"  = ".$this->ProductID
			)
		){
			return $product;
		}
		return false;
	}

	public function onPlacement() {
		parent::onPlacement();
		if($product = $this->Product(true)){
			$this->ProductVersion = $product->Version;
		}
	}

	public function TableTitle() {
		$product = $this->Product();
		$tabletitle = ($product) ? $product->Title : $this->i18n_singular_name();
		$this->extend('updateTableTitle', $tabletitle);
		return $tabletitle;
	}

	public function Link() {
		if($product = $this->Product()){
			return $product->Link();
		}
	}

}

There is also an extension file that adds two further fields where editors can add product descriptions which show in different page views:

<?php

class ProductExtension extends DataExtension{
   
   private static $db = array(
      'Summary' => 'HTMLText',
      'Facts' => 'HTMLText'
   );

   public function updateCMSFields(FieldList $fields) {
      $fields->addFieldToTab("Root.Summary",
         new HTMLEditorField("Summary")
      );
      
      $fields->addFieldToTab("Root.Facts",
         new HTMLEditorField("Facts")
      );
      
   }

}

Avatar
Nivanka

Community Member, 400 Posts

28 October 2014 at 3:58pm

I did try installing the ss-shop on a silverstripe 3.1.6, and it all just worked fine.

the only thing i can ask is that how did you add the extension to the class, is that by adding theese to the config.yml

Product:
    extensions:
        - 'ProductExtension'

and its weird if it removed the CMS actions, are you using any other modules ? or good to check whether there are any sort of javascript errors too.

Go to Top