/** * Return the columns to export * * @param GridField $gridField * * @return array */ protected function getExportColumnsForGridField(GridField $gridField) { if ($this->exportColumns) { return $this->exportColumns; } /** @var GridFieldDataColumns $dataCols */ $dataCols = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns'); if ($dataCols) { return $dataCols->getDisplayFields($gridField); } return DataObject::singleton($gridField->getModelClass())->summaryFields(); }
/** * Given a relation declared on a remote class, generate a substitute component for the opposite * side of the relation. * * Notes on behaviour: * - This can still be used on components that are defined on both sides, but do not need to be. * - All has_ones on remote class will be treated as local has_many, even if they are belongs_to * - Cannot be used on polymorphic relationships * - Cannot be used on unsaved objects. * * @param string $remoteClass * @param string $remoteRelation * @return DataList|DataObject The component, either as a list or single object * @throws BadMethodCallException * @throws InvalidArgumentException */ public function inferReciprocalComponent($remoteClass, $remoteRelation) { $remote = DataObject::singleton($remoteClass); $class = $remote->getRelationClass($remoteRelation); $schema = static::getSchema(); // Validate arguments if (!$this->isInDB()) { throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects"); } if (empty($class)) { throw new InvalidArgumentException(sprintf("%s invoked with invalid relation %s.%s", __METHOD__, $remoteClass, $remoteRelation)); } if ($class === self::class) { throw new InvalidArgumentException(sprintf("%s cannot generate opposite component of relation %s.%s as it is polymorphic. " . "This method does not support polymorphic relationships", __METHOD__, $remoteClass, $remoteRelation)); } if (!is_a($this, $class, true)) { throw new InvalidArgumentException(sprintf("Relation %s on %s does not refer to objects of type %s", $remoteRelation, $remoteClass, static::class)); } // Check the relation type to mock $relationType = $remote->getRelationType($remoteRelation); switch ($relationType) { case 'has_one': // Mock has_many $joinField = "{$remoteRelation}ID"; $componentClass = $schema->classForField($remoteClass, $joinField); $result = HasManyList::create($componentClass, $joinField); if ($this->model) { $result->setDataModel($this->model); } return $result->setDataQueryParam($this->getInheritableQueryParams())->forForeignID($this->ID); case 'belongs_to': case 'has_many': // These relations must have a has_one on the other end, so find it $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic); if ($polymorphic) { throw new InvalidArgumentException(sprintf("%s cannot generate opposite component of relation %s.%s, as the other end appears" . "to be a has_one polymorphic. This method does not support polymorphic relationships", __METHOD__, $remoteClass, $remoteRelation)); } $joinID = $this->getField($joinField); if (empty($joinID)) { return null; } // Get object by joined ID return DataObject::get($remoteClass)->filter('ID', $joinID)->setDataQueryParam($this->getInheritableQueryParams())->first(); case 'many_many': case 'belongs_many_many': // Get components and extra fields from parent list($relationClass, $componentClass, $parentClass, $componentField, $parentField, $table) = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation); $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array(); // Reverse parent and component fields and create an inverse ManyManyList /** @var RelationList $result */ $result = Injector::inst()->create($relationClass, $componentClass, $table, $componentField, $parentField, $extraFields); if ($this->model) { $result->setDataModel($this->model); } $this->extend('updateManyManyComponents', $result); // If this is called on a singleton, then we return an 'orphaned relation' that can have the // foreignID set elsewhere. return $result->setDataQueryParam($this->getInheritableQueryParams())->forForeignID($this->ID); default: return null; } }
public function setUp() { //nest config and injector for each test so they are effectively sandboxed per test Config::nest(); Injector::nest(); $this->originalReadingMode = Versioned::get_reading_mode(); // We cannot run the tests on this abstract class. if (get_class($this) == __CLASS__) { $this->markTestSkipped(sprintf('Skipping %s ', get_class($this))); return; } // Mark test as being run $this->originalIsRunningTest = self::$is_running_test; self::$is_running_test = true; // i18n needs to be set to the defaults or tests fail i18n::set_locale(i18n::config()->get('default_locale')); i18n::config()->date_format = null; i18n::config()->time_format = null; // Set default timezone consistently to avoid NZ-specific dependencies date_default_timezone_set('UTC'); // Remove password validation $this->originalMemberPasswordValidator = Member::password_validator(); $this->originalRequirements = Requirements::backend(); Member::set_password_validator(null); Cookie::config()->update('report_errors', false); if (class_exists('SilverStripe\\CMS\\Controllers\\RootURLController')) { RootURLController::reset(); } if (class_exists('Translatable')) { Translatable::reset(); } Versioned::reset(); DataObject::reset(); if (class_exists('SilverStripe\\CMS\\Model\\SiteTree')) { SiteTree::reset(); } Hierarchy::reset(); if (Controller::has_curr()) { Controller::curr()->setSession(Session::create(array())); } Security::$database_is_ready = null; // Add controller-name auto-routing // @todo Fix to work with namespaced controllers Director::config()->update('rules', array('$Controller//$Action/$ID/$OtherID' => '*')); $fixtureFiles = $this->getFixturePaths(); // Todo: this could be a special test model $this->model = DataModel::inst(); // Set up fixture if ($fixtureFiles || $this->usesDatabase) { if (!self::using_temp_db()) { self::create_temp_db(); } DataObject::singleton()->flushCache(); self::empty_temp_db(); foreach ($this->requireDefaultRecordsFrom as $className) { $instance = singleton($className); if (method_exists($instance, 'requireDefaultRecords')) { $instance->requireDefaultRecords(); } if (method_exists($instance, 'augmentDefaultRecords')) { $instance->augmentDefaultRecords(); } } foreach ($fixtureFiles as $fixtureFilePath) { $fixture = YamlFixture::create($fixtureFilePath); $fixture->writeInto($this->getFixtureFactory()); } $this->logInWithPermission("ADMIN"); } // Preserve memory settings $this->originalMemoryLimit = ini_get('memory_limit'); // turn off template debugging SSViewer::config()->update('source_file_comments', false); // Clear requirements Requirements::clear(); // Set up email $this->mailer = new TestMailer(); Injector::inst()->registerService($this->mailer, 'SilverStripe\\Control\\Email\\Mailer'); Email::config()->remove('send_all_emails_to'); }
/** * Gets summary of items in changeset * * @return string */ public function getDescription() { // Initialise list of items to count $counted = []; $countedOther = 0; foreach ($this->config()->important_classes as $type) { if (class_exists($type)) { $counted[$type] = 0; } } // Check each change item /** @var ChangeSetItem $change */ foreach ($this->Changes() as $change) { $found = false; foreach ($counted as $class => $num) { if (is_a($change->ObjectClass, $class, true)) { $counted[$class]++; $found = true; break; } } if (!$found) { $countedOther++; } } // Describe set based on this output $counted = array_filter($counted); // Empty state if (empty($counted) && empty($countedOther)) { return ''; } // Put all parts together $parts = []; foreach ($counted as $class => $count) { $parts[] = DataObject::singleton($class)->i18n_pluralise($count); } // Describe non-important items if ($countedOther) { if ($counted) { $parts[] = i18n::pluralise(_t('ChangeSet.DESCRIPTION_OTHER_ITEM', 'other item'), _t('ChangeSet.DESCRIPTION_OTHER_ITEMS', 'other items'), $countedOther); } else { $parts[] = i18n::pluralise(_t('ChangeSet.DESCRIPTION_ITEM', 'item'), _t('ChangeSet.DESCRIPTION_ITEMS', 'items'), $countedOther); } } // Figure out how to join everything together if (empty($parts)) { return ''; } if (count($parts) === 1) { return $parts[0]; } // Non-comma list if (count($parts) === 2) { return _t('ChangeSet.DESCRIPTION_AND', '{first} and {second}', ['first' => $parts[0], 'second' => $parts[1]]); } // First item $string = _t('ChangeSet.DESCRIPTION_LIST_FIRST', '{item}', ['item' => $parts[0]]); // Middle items for ($i = 1; $i < count($parts) - 1; $i++) { $string = _t('ChangeSet.DESCRIPTION_LIST_MID', '{list}, {item}', ['list' => $string, 'item' => $parts[$i]]); } // Oxford comma $string = _t('ChangeSet.DESCRIPTION_LIST_LAST', '{list}, and {item}', ['list' => $string, 'item' => end($parts)]); return $string; }
/** * @return SearchContext */ public function getSearchContext() { $context = DataObject::singleton($this->modelClass)->getDefaultSearchContext(); // Namespace fields, for easier detection if a search is present foreach ($context->getFields() as $field) { $field->setName(sprintf('q[%s]', $field->getName())); } foreach ($context->getFilters() as $filter) { $filter->setFullName(sprintf('q[%s]', $filter->getFullName())); } $this->extend('updateSearchContext', $context); return $context; }
/** * Build item resource from a changesetitem * * @param ChangeSetItem $changeSetItem * @return array */ protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) { $baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass); $baseSingleton = DataObject::singleton($baseClass); $thumbnailWidth = (int) $this->config()->thumbnail_width; $thumbnailHeight = (int) $this->config()->thumbnail_height; $hal = ['_links' => ['self' => ['href' => $this->ItemLink($changeSetItem->ID)]], 'ID' => $changeSetItem->ID, 'Created' => $changeSetItem->Created, 'LastEdited' => $changeSetItem->LastEdited, 'Title' => $changeSetItem->getTitle(), 'ChangeType' => $changeSetItem->getChangeType(), 'Added' => $changeSetItem->Added, 'ObjectClass' => $changeSetItem->ObjectClass, 'ObjectID' => $changeSetItem->ObjectID, 'BaseClass' => $baseClass, 'Singular' => $baseSingleton->i18n_singular_name(), 'Plural' => $baseSingleton->i18n_plural_name(), 'Thumbnail' => $changeSetItem->ThumbnailURL($thumbnailWidth, $thumbnailHeight)]; // Get preview urls $previews = $changeSetItem->getPreviewLinks(); if ($previews) { $hal['_links']['preview'] = $previews; } // Get edit link $editLink = $changeSetItem->CMSEditLink(); if ($editLink) { $hal['_links']['edit'] = ['href' => $editLink]; } // Depending on whether the object was added implicitly or explicitly, set // other related objects. if ($changeSetItem->Added === ChangeSetItem::IMPLICITLY) { $referencedItems = $changeSetItem->ReferencedBy(); $referencedBy = []; foreach ($referencedItems as $referencedItem) { $referencedBy[] = ['href' => $this->SetLink($referencedItem->ID)]; } if ($referencedBy) { $hal['_links']['referenced_by'] = $referencedBy; } } return $hal; }
/** * Get the whole tree of a part of the tree via an AJAX request. * * @param HTTPRequest $request * @return string * @throws Exception */ public function tree(HTTPRequest $request) { // Array sourceObject is an explicit list of values - construct a "flat tree" if (is_array($this->sourceObject)) { $output = "<ul class=\"tree\">\n"; foreach ($this->sourceObject as $k => $v) { $output .= '<li id="selector-' . $this->name . '-' . $k . '"><a>' . $v . '</a>'; } $output .= "</ul>"; return $output; } // Regular source specification $isSubTree = false; $this->search = $request->requestVar('search'); $id = is_numeric($request->latestParam('ID')) ? (int) $request->latestParam('ID') : (int) $request->requestVar('ID'); /** @var DataObject|Hierarchy $obj */ $obj = null; if ($id && !$request->requestVar('forceFullTree')) { $obj = DataObject::get_by_id($this->sourceObject, $id); $isSubTree = true; if (!$obj) { throw new Exception("TreeDropdownField->tree(): the object #{$id} of type {$this->sourceObject} could not be found"); } } else { if ($this->baseID) { $obj = DataObject::get_by_id($this->sourceObject, $this->baseID); } if (!$this->baseID || !$obj) { $obj = DataObject::singleton($this->sourceObject); } } // pre-process the tree - search needs to operate globally, not locally as marking filter does if ($this->search) { $this->populateIDs(); } if ($this->filterCallback || $this->search) { $obj->setMarkingFilterFunction(array($this, "filterMarking")); } $obj->markPartialTree($nodeCountThreshold = 30, $context = null, $this->childrenMethod, $this->numChildrenMethod); // allow to pass values to be selected within the ajax request if (isset($_REQUEST['forceValue']) || $this->value) { $forceValue = isset($_REQUEST['forceValue']) ? $_REQUEST['forceValue'] : $this->value; $values = preg_split('/,\\s*/', $forceValue); if ($values) { foreach ($values as $value) { if (!$value || $value == 'unchanged') { continue; } $obj->markToExpose($this->objectForKey($value)); } } } $self = $this; $titleFn = function (&$child) use(&$self) { /** @var DataObject|Hierarchy $child */ $keyField = $self->keyField; $labelField = $self->labelField; return sprintf('<li id="selector-%s-%s" data-id="%s" class="class-%s %s %s"><a rel="%d">%s</a>', Convert::raw2xml($self->getName()), Convert::raw2xml($child->{$keyField}), Convert::raw2xml($child->{$keyField}), Convert::raw2xml($child->class), Convert::raw2xml($child->markingClasses($self->numChildrenMethod)), $self->nodeIsDisabled($child) ? 'disabled' : '', (int) $child->ID, $child->obj($labelField)->forTemplate()); }; // Limit the amount of nodes shown for performance reasons. // Skip the check if we're filtering the tree, since its not clear how many children will // match the filter criteria until they're queried (and matched up with previously marked nodes). $nodeThresholdLeaf = Config::inst()->get('SilverStripe\\ORM\\Hierarchy\\Hierarchy', 'node_threshold_leaf'); if ($nodeThresholdLeaf && !$this->filterCallback && !$this->search) { $className = $this->sourceObject; $nodeCountCallback = function ($parent, $numChildren) use($className, $nodeThresholdLeaf) { if ($className === 'SilverStripe\\CMS\\Model\\SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) { return sprintf('<ul><li><span class="item">%s</span></li></ul>', _t('LeftAndMain.TooManyPages', 'Too many pages')); } return null; }; } else { $nodeCountCallback = null; } if ($isSubTree) { $html = $obj->getChildrenAsUL("", $titleFn, null, true, $this->childrenMethod, $this->numChildrenMethod, true, null, $nodeCountCallback); return substr(trim($html), 4, -5); } else { $html = $obj->getChildrenAsUL('class="tree"', $titleFn, null, true, $this->childrenMethod, $this->numChildrenMethod, true, null, $nodeCountCallback); return $html; } }
/** * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for * a list of record IDs, for more efficient database querying. If $idList * is null, then every record will be pre-cached. * * @param string $class * @param string $stage * @param array $idList */ public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) { if (!Config::inst()->get('SilverStripe\\ORM\\Versioning\\Versioned', 'prepopulate_versionnumber_cache')) { return; } $filter = ""; $parameters = array(); if ($idList) { // Validate the ID list foreach ($idList as $id) { if (!is_numeric($id)) { user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id, E_USER_ERROR); } } $filter = 'WHERE "ID" IN (' . DB::placeholders($idList) . ')'; $parameters = $idList; } /** @var Versioned|DataObject $singleton */ $singleton = DataObject::singleton($class); $baseClass = $singleton->baseClass(); $baseTable = $singleton->baseTable(); $stageTable = $singleton->stageTable($baseTable, $stage); $versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"{$stageTable}\" {$filter}", $parameters)->map(); foreach ($versions as $id => $version) { self::$cache_versionnumber[$baseClass][$stage][$id] = $version; } }
/** * @param array $properties * @return string */ public function Field($properties = array()) { Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jstree/jquery.jstree.js'); Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/TreeDropdownField.js'); Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css'); Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/TreeDropdownField.css'); $item = DataObject::singleton($this->sourceObject); $emptyTitle = _t('DropdownField.CHOOSE_MODEL', '(Choose {name})', ['name' => $item->i18n_singular_name()]); $record = $this->Value() ? $this->objectForKey($this->Value()) : null; if ($record instanceof ViewableData) { $title = $record->obj($this->labelField)->forTemplate(); } elseif ($record) { $title = Convert::raw2xml($record->{$this->labelField}); } else { $title = $emptyTitle; } // TODO Implement for TreeMultiSelectField $metadata = array('id' => $record ? $record->ID : null, 'ClassName' => $record ? $record->ClassName : $this->sourceObject); $properties = array_merge($properties, array('Title' => $title, 'EmptyTitle' => $emptyTitle, 'Metadata' => $metadata ? Convert::raw2json($metadata) : null)); return parent::Field($properties); }