I have relation between BlogArticle and BlogCategory many_many & belongs_many_many. I would like to add CheckboxSetField or ListBoxField to cmsFields on BlogArticle, which contains BlogCategories.
Following code shows correct checkboxes in cms, but from some reason it doesn't store the values:
class BlogCategory extends DataObject
{
private static $db = [
'Title' => 'Varchar(255)'
];
private static $belongs_many_many = [
'BlogArticles' => BlogArticle::class
];
}
class BlogArticle extends Page
{
private static $many_many = [
"BlogCategories" => BlogCategory::class,
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
$field = CheckboxSetField::create(
'BlogCategories',
'Categories',
BlogCategory::get()
);
$fields->add($field);
return $fields;
}
}
Any ideas what's wrong? Thanks a lot!
On your BlogArticle.php you aren’t referencing the relationship so it can’t save.
So BlogCategory::get() should be $this->BlogCategories() - you will probably have to map() the values aswell.
There’s an example of using the checkbox field with a $many_many here: https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-many-many-1
I want to be able to associate team members to a project using the ListBoxField . I have a ProjectHolder which has Project pages as it's children. I also have a TeamHolder which has TeamPage's as its children.
I want to be able to save multiple team members in the ListBoxField and then loop through them on a Project page. I also want to be able to link to the team members pages. e.g
<% loop $TeamMemberNames %>
$Name
<% end_loop %>
My Current code:
TeamPage.php
class TeamPage extends Page
{
private static $db = array(
'Name' => 'Varchar(255)',
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.TeamMembers', TextField::create('Name'));
return $fields;
}
}
Project.php
class Project extends Page
{
private static $db = array(
'Name' => 'Varchar(255)',
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.TeamMembers', TextField::create('Name'));
$fields->addFieldToTab('Root.TeamMembers', ListBoxField::create(
'TeamPage',
'Select Team Members for project',
TeamPage::get()->map("ID", "Name")->toArray()
)->setMultiple(true));
return $fields;
}
}
Screenshot:
I can pull through the names from the TeamPage Object into the ListBoxField however after selecting the names I need a way to save the multiple values and also get the Link so I can link to the appropriate team members pages that are listed.
To save data to the database we must first define a relationship to the data in our class. In this case we want to create a $many_many relationship between Project and TeamPage.
In the Project class we add a $many_many relationship to TeamPage. Here, the relationship name is TeamPages.
When creating the ListBoxField we pass the TeamPages relationship name so the field knows where to save this data:
Project.php
class Project extends Page
{
private static $db = array(
'Name' => 'Varchar(255)'
);
private static $many_many = array(
'TeamPages' => 'TeamPage'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.TeamMembers', TextField::create('Name'));
$fields->addFieldToTab('Root.TeamMembers', ListBoxField::create(
'TeamPages',
'Select Team Members for project',
TeamPage::get()->map('ID', 'Name')->toArray()
)->setMultiple(true));
return $fields;
}
}
Now in our template we can loop through a Project's $TeamPages by calling the following:
Template
<% loop $TeamPages %>
$Name
<% end_loop %>
If we would like a TeamPage to be able to access it's related Projects we can add a $belongs_many_many to the TeamPage class to point back to the Project class. You can also add a ListBoxField to control Projects from the TeamPage.
TeamPage.php
class TeamPage extends Page
{
private static $db = array(
'Name' => 'Varchar(255)'
);
private static $belongs_many_many = array(
'Projects' => 'Project'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.TeamMembers', TextField::create('Name'));
$fields->addFieldToTab('Root.TeamMembers', ListBoxField::create(
'Projects',
'Select project for this team page',
Project::get()->map('ID', 'Name')->toArray()
)->setMultiple(true));
return $fields;
}
}
Something to note is TeamPage and Project both extend Page. This means both classes inherit a Title field. I would suggest using Title instead of Name, unless you have a specific reason to do so.
This would make the code:
Project.php
class Project extends Page
{
private static $many_many = array(
'TeamPages' => 'TeamPage'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.TeamMembers', ListBoxField::create(
'TeamPages',
'Select Team Members for project',
TeamPage::get()->map('ID', 'Title')->toArray()
)->setMultiple(true));
return $fields;
}
}
TeamPage.php
class TeamPage extends Page
{
private static $belongs_many_many = array(
'Projects' => 'Project'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Projects', ListBoxField::create(
'Projects',
'Select project for this team page',
Project::get()->map('ID', 'Title')->toArray()
)->setMultiple(true));
return $fields;
}
}
Template
<% loop $TeamPages %>
$Title
<% end_loop %>
In SilverStripe 3.1 is it possible to add different tabs and fields on the about page for example.
And then different tabs and fields on a services page for example.
About Page - Images Tab / Attachments Tab
Services Page - Images Tab / Attachments Tab / Staff Person Tab
The following code is an example. I have added the if statements around a snippet that does work. But it only seems to work for all pages by showing the same tabs on all pages.
I've been doing the video lessons on the SilverStripe website and I can see that you can create page types but I really need to know if you can achieve this without having to create extra page types.
// I want this on the about page
// if page=about {
class Page extends SiteTree {
private static $has_one = array (
'Photo' => 'image',
'Brochure' => 'file',
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Images', $photo = UploadField::create('Photo'));
$fields->addFieldToTab('Root.Attachments', $brochure = UploadField::create('Brochure'));
return $fields;
}
}
// I want this on the services page
// } elseif page=services {
class Page extends SiteTree {
private static $has_one = array (
'Photo' => 'image',
'Brochure' => 'file',
'Staff Person' => 'image',
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Images', $photo = UploadField::create('Photo'));
$fields->addFieldToTab('Root.Attachments', $brochure = UploadField::create('Brochure'));
$fields->addFieldToTab('Root.Staff', $staff = UploadField::create('Staff'));
return $fields;
}
}
// }
class Page_Controller extends ContentController {
private static $allowed_actions = array();
public function init() {
parent::init();
}
}
I would recommend to use individual page types for what you want to do.
However, if you only want to use one page type you can use an if statement in your getCMSFields function to display different fields.
In this example code I check the URLSegment, although you could check something else like Title.
class Page extends SiteTree {
private static $has_one = array (
'Photo' => 'image',
'Brochure' => 'file',
'Staff Person' => 'image',
);
public function getCMSFields() {
$fields = parent::getCMSFields();
if ($this->URLSegment == 'about' || $this->URLSegment == 'services') {
$fields->addFieldToTab('Root.Images', $photo = UploadField::create('Photo'));
$fields->addFieldToTab('Root.Attachments', $brochure = UploadField::create('Brochure'));
}
if ($this->URLSegment == 'services') {
$fields->addFieldToTab('Root.Staff', $staff = UploadField::create('Staff'));
}
return $fields;
}
}
I have a DataObject called ContentSection that has 2 has_one relationships: to a page type LandingPage and another DataObject Person.
class ContentSection extends DataObject {
protected static $has_one = array(
'Person' => 'Person',
'LandingPage' => 'LandingPage'
);
}
Both LandingPage and Person define a has_many relationship to ContentSection.
class LandingPage extends Page {
protected static $has_many = array(
'ContentSections' => 'ContentSection'
);
}
class Person extends DataObject {
protected static $has_many = array(
'ContentSections' => 'ContentSection'
);
}
ContentSections are editable through the LandingPage and Person with the GridFieldConfig_RelationEditor e.g.:
function getCMSFields() {
$fields = parent::getCMSFields();
$config = GridFieldConfig_RelationEditor::create(10);
$fields->addFieldToTab('Root.Content', new GridField('ContentSections', 'Content Sections', $this->ContentSections(), $config));
return $fields;
}
My question is how can you hide/remove irrelevant has_one fields in the CMS editor tab? The Person and LandingPage relationship dropdown fields both display when you are editing a ContentSection, whether it is for a Person or LandingPage. I only want to show the relevant has_one relationship field. I've tried using dot notation on the has_many relationships:
class Person extends DataObject {
protected static $has_many = array(
'ContentSections' => 'ContentSection.Person'
);
}
I've also tried using the removeFieldFromTab method in the getCMSFields method of the ContentSection class, where I define the other CMS fields for the ContentSection:
$fields->removeFieldFromTab('Root.Main', 'Person');
Instead of removeFieldFromTab use the removeByName function. removeFieldFromTab will not work if there is no 'Root.Main' tab.
Also we remove PersonID, not Person. has_one variables have ID appended to the end of their variable name.
function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName('PersonID');
$fields->removeByName('LandingPageID');
return $fields;
}
If you would like to selectively hide or display these fields you can put in some if statements in your getCMSFields function.
function getCMSFields() {
$fields = parent::getCMSFields();
if (!$this->PersonID) {
$fields->removeByName('PersonID');
}
if (!$this->LandingPageID) {
$fields->removeByName('LandingPageID');
}
return $fields;
}
I'm learning SilverStripe by creating a small website that lets the user manage their fragrances (i.e. perfumes/colognes). The user adds ingredients (that are used in the fragrances they have), then adds their fragrances, at which point they choose which ingredients are in the fragrance they're adding.
I've created the Ingredient and Fragrance classes which both extend DataObject. I've also created the IngredientsPage page which lets the user add/edit/delete ingredients (made up of a name and description) and lists all of the ingredients added so far, and this page is fully functional. I'm now trying to create the FragrancesPage page which will let the user add/edit/delete fragrances (made up of a name, description and ingredients) and list all the ones added so far, but I'm having some trouble.
The only way I know of to create the relationship between a Fragrance and Ingredients (one fragrance has many ingredients, and one ingredient belongs to many fragrances) is using a GridField (if there's a better way, let me know!), as this is what the SilverStripe tutorials get you to do (although in the tutorial it's for the CMS rather than the front-end). However, as soon as I try to add a GridField into the mix, I just get taken to an error page that says "Server Error: Sorry, there was a problem with handling your request.".
My code is as follows.
Ingredient.php:
<?php
class Ingredient extends DataObject {
private static $db = array(
'Name' => 'Text',
'Description' => 'Text'
);
private static $belongs_many_many = array(
'Fragrances' => 'Fragrance'
);
}
?>
Fragrance.php:
<?php
class Fragrance extends DataObject {
private static $db = array(
'Name' => 'Text',
'Description' => 'Text'
);
private static $many_many = array(
'Ingredients' => 'Ingredient'
);
}
?>
FragrancesPage.php:
<?php
class FragrancesPage extends Page {
private static $icon = 'cms/images/treeicons/reports-file.png';
private static $description = 'Fragrances page';
}
class FragrancesPage_Controller extends Page_Controller {
private static $allowed_actions = array('FragranceAddForm');
function FragranceAddForm() {
$config = GridFieldConfig_RelationEditor::create();
$config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
'Name' => 'Name',
'Ingredient.Name' => 'Ingredient'
));
$fragrances_field = new GridField(
'Ingredients',
'Ingredient',
$this->Ingredients(),
$config
);
$fields = new FieldList(
new TextField('Name', 'Fragrance Name'),
new TextareaField('Description', 'Fragrance Description'),
$fragrances_field
);
$actions = new FieldList(
new FormAction('doFragranceAdd', 'Add Fragrance')
);
$validator = new RequiredFields('Name', 'Description');
return new Form($this, 'FragranceAddForm', $fields, $actions, $validator);
}
public function doFragranceAdd($data, $form) {
$submission = new Fragrance();
$form->saveInto($submission);
$submission->write();
return $this->redirectBack();
}
public function FragranceList() {
$submissions = Fragrance::get()->sort('Name');
return $submissions;
}
}
?>
If I remove everything from FragrancesPage.php relating to the GridField, the page works fine. I just can't seem to get the GridField working, and don't know of any other way to create the relationship between Fragrances and Ingredients on the front-end. If the code for IngredientsPage.php will be helpful also, let me know and I'll add it.
my guess is that you have error reporting turned off, and that is why you only see such a meaning less error message.
you should turn on display_errors and error_reporting in your php.ini, .htaccess or _ss_environment.php (IMPORTANT: setting it in _config.php will NOT work, as it gets overwritten by the error handler)
the problem I see in your code is that trying to use $this->Ingredients() on FragrancesPage, but as far as I can see only the class Fragrances has a method Ingredients (method is 'magically' created for the many_many relation).
also, I think your setDisplayFields()
so basically you need to use $fragrance->Ingredients() instead of $this->Ingredients().
but that leads us to the next problem: you don't have a fragrance yet.
unfortunately, at this time, GridField only works if you already have an object. this means you have to split it into two forms or use an alternative.
Option 1: use CheckboxSetField to manage the many_many relation.
this will not allow creating Ingredients on the fly, it will only give you check boxes that you can tick to link the items.
public function FragranceAddForm() {
$fragrances_field = new CheckboxSetField('Ingredients', 'Ingredient', Ingredient::get()->map());
$fields = new FieldList(
new TextField('Name', 'Fragrance Name'),
new TextareaField('Description', 'Fragrance Description'),
$fragrances_field
);
$actions = new FieldList(
new FormAction('doFragranceAdd', 'Add Fragrance')
);
$validator = new RequiredFields('Name', 'Description');
return new Form($this, __FUNCTION__, $fields, $actions, $validator);
}
public function doFragranceAdd($data, $form) {
$submission = new Fragrance();
$form->saveInto($submission);
$submission->write();
return $this->redirectBack();
}
Option 2: use GridField in a second form
this will allow creating Ingredients on the fly, but is a bit more work. and you might run into some troubles with GridField as it is not fully tested in the frontend yet.
(there is a recent question where I wrote a bit about GridField problems in frontends https://stackoverflow.com/a/22059197/1119263)
I guess this place is as good as any to finally write a tutorial/working example for frontend GridFields.
I took the liberty of refactoring your code a bit to include a edit functionality, its much nicer to have the GridField on the edit page than on a separate form.
As mentioned before, the GridField is not working that well in the frontend, there is a module to ease the pain, but it will still be rough around the edges and will require you to do some styling to make it look pretty.
Find the module on Packagist or GitHub
(you will need t
class Ingredient extends DataObject {
private static $db = array(
'Name' => 'Text',
'Description' => 'Text'
);
private static $belongs_many_many = array(
'Fragrances' => 'Fragrance'
);
public function getCMSFields() {
// fields used by the GridField, don't let the CMSFields mislead you
return new FieldList(
TextField::create('Name', 'Name'),
TextAreaField::create('Description', 'Description')
);
}
}
class Fragrance extends DataObject {
private static $db = array(
'Name' => 'Text',
'Description' => 'Text'
);
private static $many_many = array(
'Ingredients' => 'Ingredient'
);
}
/**
* Form in a separate class, so we can reuse it.
* #param Controller $controller
* #param string $name
* #param Null|Fragrance $fragrance Either null to create a new one, or pass an existing to edit it and add Ingredients
* #return Form
*/
class FragranceForm extends Form {
public function __construct($controller, $name, $fragrance = null) {
if ($fragrance && $fragrance->isInDB()) {
// we can only use a GridField if the object exists and has already been saved
// gridfield needs jQuery
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.min.js');
// ensure we don't have 2 versions of jQuery
Requirements::block(THIRDPARTY_DIR . '/jquery/jquery.js');
$config = FrontEndGridFieldConfig_RelationEditor::create();
$config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
'Name' => 'Name',
'Description' => 'Description',
));
$ingredientField = new FrontEndGridField(
'Ingredients',
'Ingredient',
$fragrance->Ingredients(),
$config
);
} else {
$ingredientField = new LiteralField('Ingredients', '<p>Ingredients can be added after saving</p>');
}
$fields = new FieldList(
new HiddenField('ID', ''),
new TextField('Name', 'Fragrance Name'),
new TextareaField('Description', 'Fragrance Description'),
$ingredientField
);
$actions = new FieldList(
new FormAction('doFragranceSave', 'Save Fragrance')
);
$validator = new RequiredFields('Name', 'Description');
// populate the fields (ID, Name and Description) with the values from $fragrance. This does not effect the GridField
if ($fragrance && $fragrance->exists()) {
$fields->fieldByName('ID')->setValue($fragrance->ID);
$fields->fieldByName('Name')->setValue($fragrance->Name);
$fields->fieldByName('Description')->setValue($fragrance->Description);
// there is actually a method for that, but we can't use it here,
// because fields are not set yet. we could do it after __construct, but then we would
// overwrite things set by the error handler, so lets just do it by hand
// $this->loadDataFrom($fragrance);
}
parent::__construct($controller, $name, $fields, $actions, $validator);
}
public function doFragranceSave($data, $form) {
if (isset($data['ID']) && $data['ID']) {
$id = (int)$data['ID'];
$fragrance = Fragrance::get()->byID($id);
}
if (!isset($fragrance) || !$fragrance || !$fragrance->exists()) {
// if the ID was invalid or we don't have one, create a new Fragrance
$fragrance = new Fragrance();
}
$form->saveInto($fragrance);
$fragrance->write();
// redirect to the edit page.
$controller = $this->getController();
$editLink = $controller->EditLink($fragrance->ID);
return $controller->redirect($editLink);
}
}
class FragrancesPage extends Page {
}
class FragrancesPage_Controller extends Page_Controller {
private static $allowed_actions = array(
'edit',
'AddForm',
'EditForm',
);
/**
* the default action
* #return ViewableData_Customised
*/
public function index() {
// $this->customise() lets you overwrite variables that you can use in the template later.
return $this->customise(array(
// set the AddForm to $Form instead of $AddForm, this way you can use $Form in template and can reuse the template
'Form' => $this->AddForm(),
));
}
/**
* edit action to edit an existing Fragrance
* links will look like this /FragrancesPage/edit/$ID
*
* #param SS_HTTPRequest $request
* #return SS_HTTPResponse|ViewableData_Customised
*/
public function edit(SS_HTTPRequest $request) {
$id = (int)$request->param('ID');
$fragrance = Fragrance::get()->byID($id);
if (!$fragrance || !$fragrance->exists()) {
// fragrance not found? display a 404 error page
return ErrorPage::response_for(404);
}
// now that we have a $fragrance, overwrite EditForm with a EditForm that contains the $fragrance
$form = $this->EditForm($fragrance);
$return = $this->customise(array(
// also overwrite Title and Content, to display info about what the user can do here
// if you don't overwrite that, it will display the Title and Content of the page
'Title' => 'Edit: ' . $fragrance->Name,
'Content' => '<p>you are editing an existing fragrance</p>',
// set the Form to $Form instead of $EditForm, this way you can use $Form in template and can reuse the template
'Form' => $form,
));
// per default SilverStripe will try to use the following templates: FragrancesPage_edit.ss > FragrancesPage.ss > Page.ss
// if you want to use a custom template here, you can specify that with ->renderWith()
// but you probably won't need that anyway
// $return = $return->renderWith(array('MyCustomTemplateName', 'Page'));
return $return;
}
public function AddForm() {
return new FragranceForm($this, __FUNCTION__);
}
public function EditForm($fragranceOrRequest = null) {
// unfortunately, GridField / FormFields in general are a bit clumsy and do forget what item they where
// suppose to edit, so we have to check what $fragranceOrRequest is and set/get the fragrance to/from session
if ($fragranceOrRequest && is_a($fragranceOrRequest, 'Fragrance')) {
$fragrance = $fragranceOrRequest;
Session::set('FragrancesPage.CurrentFragrance', $fragrance->ID);
} else {
$fragrance = Fragrance::get()->byID(Session::get('FragrancesPage.CurrentFragrance'));
}
if (!$fragrance || !$fragrance->exists()) {
// that's bad, some error has occurred, lets display an ugly 404 page
return $this->httpError(404);
}
return new FragranceForm($this, __FUNCTION__, $fragrance);
}
public function EditLink($ID) {
return $this->Link("edit/$ID");
}
}
I know that's a lot to take in as someone still learning SilverStripe, if you have any questions, feel free to comment or just poke me on IRC