diff --git a/docs/en/migrating-from-single-language.md b/docs/en/migrating-from-single-language.md index 6fce063c..5137e43d 100644 --- a/docs/en/migrating-from-single-language.md +++ b/docs/en/migrating-from-single-language.md @@ -4,12 +4,14 @@ In case you want to add fluent to an existing site to add multi language functio ## Install fluent -use composer to install fluent, see [installation](installation.md) +Use composer to install fluent, see [installation](installation.md) ## Configure fluent -* add locales -You can either do this in the backend, or for the first setup you can utitlise `default_records` to add the locales to the db. +* Add locales + +You can either do this in the backend, or for the first setup you can utitlise `default_records` to add the locales to +the db. A fluent.yml might look like: ``` @@ -33,32 +35,121 @@ TractorCow\Fluent\Model\Locale: When you run `dev/build?flush` again, this adds the records to the database if the locales table is still empty. -## Publish available pages in your default locale +## Populating initial localised content for existing Pages and DataObjects in your default locale -Now your site is broken, cause no pages have been published and added as translated page in your default locale. -You can either publish all pages manually or use [publishall](https://docs.silverstripe.org/en/4/developer_guides/debugging/url_variable_tools/#building-and-publishing-urls) to publish all pages in bulk. -If you run `/admin/pages/publishall` in your browser your site will be fixed again and you can start adding translated content. +Now your site is broken because nothing has been published and added as translated data in your default locale. You can +either manually localise all DataObjects & Pages manually or use one of the automation options below. ### Automated tools for localisation -`InitialPageLocalisation` dev task can be used to either only localise or localise & publish your pages. -This dev task can be run either via CLI or queued as a job if Queued jobs module is installed. - -Localise only example - -``` -dev/tasks/initial-page-localisation-task -``` - -Localise & publish example - -``` -dev/tasks/initial-page-localisation-task publish=1 -``` - -Localisation in batches can be done by using the `limit` option. -Example below will localise & publish five pages on each run. - -``` -dev/tasks/initial-page-localisation-task publish=1&limit=5 -``` +#### From the CMS (SiteTree only) + +Use Silverstripe's +built-in [publishall](https://docs.silverstripe.org/en/4/developer_guides/debugging/url_variable_tools/#building-and-publishing-urls) +tool to publish all Pages in bulk. +Run `/admin/pages/publishall` in your browser and your site will be fixed again and you can start adding translated +content. + +_This method will work with Pages only (not localised DataObjects)._ + +#### Commandline or Queued Jobs (SiteTree and DataObjects) + +The `InitialPageLocalisation` and `InitialDataObjectLocalisationTask` dev tasks may be used to localise and, optionally, +publish your `Versioned` data (including Pages) from the commandline or queued as a job (if the Queued Jobs module is installed). + +`InitialPageLocalisation` - localise all `SiteTree` objects (Pages) + +`InitialDataObjectLocalisationTask` - localise all Fluent-enabled DataObjects (excluding `SiteTree`) + +1. Example: Localise all Pages (default, without publishing) + + ``` + dev/tasks/initial-page-localisation-task + ``` + +2. Example: Localise & publish all Pages + + ``` + dev/tasks/initial-page-localisation-task publish=1 + ``` + +3. Example: Localising Pages in batches can be done by using the `limit` option. + This will localise & publish five pages on each run. + + ``` + dev/tasks/initial-page-localisation-task publish=1&limit=5 + ``` + +4. Example: All the same functionality is available for localising all DataObjects, including `Versioned` and non-Versioned classes + + ``` + dev/tasks/initial-dataobject-localisation-task + ``` + or + + ``` + dev/tasks/initial-dataobject-localisation-task publish=1&limit=5 + ``` + +#### Customize your own initialisation dev task + +Perhaps you want to be more selective in how you initialise your localised content. +The `InitialDataObjectLocalisationTask` class can be easily extended to either list exactly which classes you want to +initially localise, or you can exclude specific classes from initialisation. + +1. **Initialise specific classes:** The following example will create a task which localises **_ONLY_** `BlogPost` +pages, `Testimonial` objects, _and their subclasses (if any)_. + + ```php + class CustomLocalisationTask extends InitialDataObjectLocalisationTask + { + /** + * @var string + */ + private static $segment = 'custom-localisation-initialisation-task'; + + /** + * @var string + */ + protected $title = 'Custom localisation initialisation'; + + /** + * @var string[] + */ + protected array $include_only_classes = [ + \SilverStripe\Blog\Model\BlogPost::class, + \AcmeCo\Model\Testimonial::class + ]; + + } + ``` + +2. **Initialise all DataObjects but exclude some:** The following example will create a task which localises **_ALL_** +DataObjects **_except_** `BlogPost` pages, `Testimonial` objects, _and their subclasses (if any)_. + + ```php + class CustomLocalisationTask extends InitialDataObjectLocalisationTask + { + /** + * @var string + */ + private static $segment = 'custom-localisation-initialisation-task'; + + /** + * @var string + */ + protected $title = 'Custom localisation initialisation'; + + /** + * @var string[] + */ + protected array $exclude_classes = [ + \SilverStripe\Blog\Model\BlogPost::class, + \AcmeCo\Model\Testimonial::class + ]; + + } + ``` + +3. **One or the other:** You may specify `$include_only_classes` OR `$exclude_classes` - not both. +If `$include_only_classes` is not an empty array, `$exclude_classes` will be ignored. diff --git a/src/Extension/FluentExtension.php b/src/Extension/FluentExtension.php index ff4f6820..06ab9507 100644 --- a/src/Extension/FluentExtension.php +++ b/src/Extension/FluentExtension.php @@ -582,11 +582,11 @@ public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) // Apply substitutions $localisedPredicate = str_replace($conditionSearch, $conditionReplace, $predicate); - + if (empty($localisedPredicate)) { continue; } - + $where[$index] = [ $localisedPredicate => $parameters ]; @@ -630,6 +630,32 @@ public function onAfterWrite(): void $this->handleClassChanged(); } + /** + * If an object is duplicated also duplicate existing localised values from original to new object. + */ + public function onAfterDuplicate($original, $doWrite, $relations): void + { + $localisedTables = $this->owner->getLocalisedTables(); + foreach ($localisedTables as $tableName => $fields) { + // Target IDs + $fromID = $original->ID; + $toID = $this->owner->ID; + + // Get localised table + $localisedTable = $this->getLocalisedTable($tableName); + + // Remove existing translations from duplicated object + DB::prepared_query("DELETE FROM \"$localisedTable\" WHERE \"RecordID\" = ?", [$toID]); + + // Copy translations to duplicated object + $fields_str = '"' . implode('","', $fields) . '"'; + DB::prepared_query("INSERT INTO \"$localisedTable\" ( \"RecordID\", \"Locale\", $fields_str) + SELECT ? AS \"RecordID\", \"Locale\", $fields_str + FROM \"$localisedTable\" + WHERE \"RecordID\" = ?", [$toID, $fromID]); + } + } + /** * If an object is changed to another class, we should trigger localised copy * @@ -1115,6 +1141,22 @@ public function updateFluentCMSField(FormField $field) $field->setTitle($tooltip); } + /** + * Update preview link to null if the object isn't in the current locale + * and we can't fallback cleanly. + * + * @param ?string $link + */ + public function updatePreviewLink(&$link): void + { + $owner = $this->owner; + $info = $owner->LocaleInformation(FluentState::singleton()->getLocale()); + + if (!$info->getSourceLocale()) { + $link = null; + } + } + /** * Require that this record is saved in the given locale for it to be visible * diff --git a/src/Extension/FluentVersionedExtension.php b/src/Extension/FluentVersionedExtension.php index 83ddc7df..65ca59fa 100644 --- a/src/Extension/FluentVersionedExtension.php +++ b/src/Extension/FluentVersionedExtension.php @@ -1008,4 +1008,49 @@ protected function setVersionCacheItem(string $class, string $stage, string $loc // Internally store nulls as 0 $this->versionsCache[$class][$stage][$locale][$key] = $value ?: 0; } + + /** + * If an object is duplicated also duplicate existing localised values from original to new object. + */ + public function onAfterDuplicate($original, $doWrite, $relations): void + { + parent::onAfterDuplicate($original, $doWrite, $relations); + + $localisedTables = $this->owner->getLocalisedTables(); + foreach ($localisedTables as $tableName => $fields) { + // Target IDs + $fromID = $original->ID; + $toID = $this->owner->ID; + + // Get localised table + $localisedTable = $this->getLocalisedTable($tableName) . self::SUFFIX_VERSIONS; + + // Remove existing translation versions from duplicated object + DB::prepared_query("DELETE FROM \"$localisedTable\" WHERE \"RecordID\" = ?", [$toID]); + + // Copy translations to duplicated object + $localisedFields = array_merge(['Locale', 'Version'], $fields); + $fields_str = '"' . implode('","', $localisedFields) . '"'; + + // Copy all versions of localised object + DB::prepared_query("INSERT INTO \"$localisedTable\" ( \"RecordID\", $fields_str) + SELECT ? AS \"RecordID\", $fields_str + FROM \"$localisedTable\" + WHERE \"RecordID\" = ?", [$toID, $fromID]); + + // Also copy versions of base record + $versionsTableName = $tableName . self::SUFFIX_VERSIONS; + + // Remove existing versions from duplicated object, created by onBeforeWrite + DB::prepared_query("DELETE FROM \"$versionsTableName\" WHERE \"RecordID\" = ?", [$toID]); + + // Copy all versions of base record, todo: optimize to only copy needed versions + $fields = DB::query("SELECT \"COLUMN_NAME\" FROM \"INFORMATION_SCHEMA\".\"COLUMNS\" WHERE \"TABLE_NAME\" = '$versionsTableName' AND \"COLUMN_NAME\" NOT IN('ID','RecordID')"); + $fields_str = '"' . implode('","', $fields->column()) . '"'; + DB::prepared_query("INSERT INTO \"$versionsTableName\" ( \"RecordID\", $fields_str) + SELECT ? AS \"RecordID\", $fields_str + FROM \"$versionsTableName\" + WHERE \"RecordID\" = ?", [$toID, $fromID]); + } + } } diff --git a/src/Extension/Traits/FluentAdminTrait.php b/src/Extension/Traits/FluentAdminTrait.php index 269cf5e7..63da66a3 100644 --- a/src/Extension/Traits/FluentAdminTrait.php +++ b/src/Extension/Traits/FluentAdminTrait.php @@ -107,7 +107,7 @@ protected function updateFluentActions( // Add menu button $moreOptions = Tab::create( 'FluentMenuOptions', - _t(__TRAIT__ . '.Localisation', 'Localisation') + _t('TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Localisation', 'Localisation') ); $moreOptions->addExtraClass('popover-actions-simulate'); $rootTabSet->push($moreOptions); @@ -117,7 +117,7 @@ protected function updateFluentActions( FormAction::create( 'clearFluent', _t( - __TRAIT__ . '.Label_clearFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_clearFluent', "Clear from all except '{title}'", [ 'title' => $locale->getTitle() @@ -129,7 +129,7 @@ protected function updateFluentActions( FormAction::create( 'copyFluent', _t( - __TRAIT__ . '.Label_copyFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_copyFluent', "Copy '{title}' to other locales", [ 'title' => $locale->getTitle() @@ -144,7 +144,7 @@ protected function updateFluentActions( FormAction::create( 'unpublishFluent', _t( - __TRAIT__ . '.Label_unpublishFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_unpublishFluent', 'Unpublish (all locales)' ) )->addExtraClass('btn-secondary') @@ -153,7 +153,7 @@ protected function updateFluentActions( FormAction::create( 'archiveFluent', _t( - __TRAIT__ . '.Label_archiveFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_archiveFluent', 'Unpublish and Archive (all locales)' ) )->addExtraClass('btn-outline-danger') @@ -162,7 +162,7 @@ protected function updateFluentActions( FormAction::create( 'publishFluent', _t( - __TRAIT__ . '.Label_publishFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_publishFluent', 'Save & Publish (all locales)' ) )->addExtraClass('btn-primary') @@ -172,7 +172,7 @@ protected function updateFluentActions( FormAction::create( 'deleteFluent', _t( - __TRAIT__ . '.Label_deleteFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_deleteFluent', 'Delete (all locales)' ) )->addExtraClass('btn-outline-danger') @@ -187,7 +187,7 @@ protected function updateFluentActions( FormAction::create( 'hideFluent', _t( - __TRAIT__ . '.Label_hideFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_hideFluent', "Hide from '{title}'", [ 'title' => $locale->getTitle() @@ -200,7 +200,7 @@ protected function updateFluentActions( FormAction::create( 'showFluent', _t( - __TRAIT__ . '.Label_showFluent', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.Label_showFluent', "Show in '{title}'", [ 'title' => $locale->getTitle() @@ -256,7 +256,7 @@ public function clearFluent($data, $form) }); $message = _t( - __TRAIT__ . '.ClearAllNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.ClearAllNotice', "All localisations have been cleared for '{title}'.", ['title' => $record->Title] ); @@ -292,7 +292,7 @@ public function copyFluent($data, $form) if ($locale->ID == $originalLocale->ID) { return; } - + if ($record->hasExtension(Versioned::class)) { $record->writeToStage(Versioned::DRAFT); } else { @@ -302,7 +302,7 @@ public function copyFluent($data, $form) }); $message = _t( - __TRAIT__ . '.CopyNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.CopyNotice', "Copied '{title}' to all other locales.", ['title' => $record->Title] ); @@ -336,7 +336,7 @@ public function unpublishFluent($data, $form) }); $message = _t( - __TRAIT__ . '.UnpublishNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.UnpublishNotice', "Unpublished '{title}' from all locales.", ['title' => $record->Title] ); @@ -386,7 +386,7 @@ public function archiveFluent($data, $form) $policy->delete($record); $message = _t( - __TRAIT__ . '.ArchiveNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.ArchiveNotice', "Archived '{title}' and all of its localisations.", ['title' => $record->Title] ); @@ -434,7 +434,7 @@ public function deleteFluent($data, $form) $policy->delete($record); $message = _t( - __TRAIT__ . '.DeleteNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.DeleteNotice', "Deleted '{title}' and all of its localisations.", ['title' => $record->Title] ); @@ -478,7 +478,7 @@ public function publishFluent($data, $form) }); $message = _t( - __TRAIT__ . '.PublishNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.PublishNotice', "Published '{title}' across all locales.", ['title' => $record->Title] ); @@ -508,7 +508,7 @@ public function showFluent($data, $form) $record->FilteredLocales()->add($locale); $message = _t( - __TRAIT__ . '.ShowNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.ShowNotice', "Record '{title}' is now visible in {locale}", [ 'title' => $record->Title, @@ -541,7 +541,7 @@ public function hideFluent($data, $form) $record->FilteredLocales()->remove($locale); $message = _t( - __TRAIT__ . '.HideNotice', + 'TractorCow\Fluent\Extension\Traits\FluentAdminTrait.HideNotice', "Record '{title}' is now hidden in {locale}", [ 'title' => $record->Title, diff --git a/src/Extension/Traits/FluentBadgeTrait.php b/src/Extension/Traits/FluentBadgeTrait.php index f47d7802..339b7612 100644 --- a/src/Extension/Traits/FluentBadgeTrait.php +++ b/src/Extension/Traits/FluentBadgeTrait.php @@ -75,7 +75,7 @@ protected function generateBadgeHTML( // If the object has been localised in the current locale, show a "localised" state $badgeClasses[] = 'fluent-badge--default'; $tooltip = _t( - __TRAIT__ . '.BadgeLocalised', + 'TractorCow\Fluent\Extension\Traits\FluentBadgeTrait.BadgeLocalised', 'Localised in {locale}', [ 'locale' => $locale->getTitle() @@ -85,7 +85,7 @@ protected function generateBadgeHTML( // If object is inheriting content from another locale show the source $badgeClasses[] = 'fluent-badge--localised'; $tooltip = _t( - __TRAIT__ . '.BadgeInherited', + 'TractorCow\Fluent\Extension\Traits\FluentBadgeTrait.BadgeInherited', 'Inherited from {locale}', [ 'locale' => $info->getSourceLocale()->getTitle() @@ -96,7 +96,7 @@ protected function generateBadgeHTML( // by either localising or seting up a locale fallback $badgeClasses[] = 'fluent-badge--invisible'; $tooltip = _t( - __TRAIT__ . '.BaggeInvisible', + 'TractorCow\Fluent\Extension\Traits\FluentBadgeTrait.BaggeInvisible', '{type} has no available content in {locale}, localise the {type} or provide a locale fallback', [ 'type' => $record->i18n_singular_name(), diff --git a/src/Extension/Traits/FluentObjectTrait.php b/src/Extension/Traits/FluentObjectTrait.php index 6206160c..b73da892 100644 --- a/src/Extension/Traits/FluentObjectTrait.php +++ b/src/Extension/Traits/FluentObjectTrait.php @@ -165,7 +165,7 @@ protected function updateFluentCMSFields(FieldList $fields) $fields ->fieldByName('Root.Locales') - ->setTitle(_t(__TRAIT__ . '.TAB_LOCALISATION', 'Localisation')); + ->setTitle(_t('TractorCow\Fluent\Extension\Traits\FluentObjectTrait.TAB_LOCALISATION', 'Localisation')); } else { $fields->push($gridField); } diff --git a/src/Middleware/InitStateMiddleware.php b/src/Middleware/InitStateMiddleware.php index 72d4a046..59925e1d 100644 --- a/src/Middleware/InitStateMiddleware.php +++ b/src/Middleware/InitStateMiddleware.php @@ -70,11 +70,6 @@ public function getIsFrontend(HTTPRequest $request) } } - // If using the CMS preview, do not treat the site as frontend - if ($request->getVar('CMSPreview')) { - return false; - } - return true; } diff --git a/src/Model/CachableModel.php b/src/Model/CachableModel.php index 2e295a86..c97bdb10 100644 --- a/src/Model/CachableModel.php +++ b/src/Model/CachableModel.php @@ -2,7 +2,6 @@ namespace TractorCow\Fluent\Model; -use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; diff --git a/src/Model/Locale.php b/src/Model/Locale.php index e97715db..0b967b2c 100644 --- a/src/Model/Locale.php +++ b/src/Model/Locale.php @@ -586,6 +586,16 @@ public function getBaseURL() return $base; } + /** + * Absolute base url + * + * @return false|string + */ + public function getAbsoluteBaseURL() + { + return Director::absoluteURL($this->getBaseURL()); + } + /** * Get other locales that appear alongside this (including self) * diff --git a/src/Task/InitialDataObjectLocalisationTask.php b/src/Task/InitialDataObjectLocalisationTask.php new file mode 100644 index 00000000..a8897e73 --- /dev/null +++ b/src/Task/InitialDataObjectLocalisationTask.php @@ -0,0 +1,287 @@ +exclude_classes`. + * @var string[] + */ + protected $include_only_classes = []; + + /** + * When extending this class, you may choose to exclude these specific classes. + * This is IGNORED if `$this->include_only_classes` is not empty. + * @var string[] + */ + protected $exclude_classes = [ + SiteTree::class + ]; + + /** + * @param HTTPRequest $request + * @return void + * @throws \ReflectionException + * @throws \SilverStripe\ORM\ValidationException + */ + public function run($request) + { + if (!Director::is_cli()) { + echo '
' . PHP_EOL; + } + + $publish = (bool)$request->getVar('publish'); + $limit = (int)$request->getVar('limit'); + + $total_results = [ + 'localisable' => 0, + 'localised' => 0, + 'publishable' => 0, + 'published' => 0, + ]; + + /** @var Locale $globalLocale */ + $globalLocale = Locale::get() + ->filter(['IsGlobalDefault' => 1]) + ->sort('ID', 'ASC') + ->first(); + + if (!$globalLocale) { + echo 'Please set global locale first!' . PHP_EOL; + + return; + } + + if ($this->include_only_classes && is_array($this->include_only_classes)) { + $classesWithFluent = $this->include_only_classes; + foreach ($this->include_only_classes as $key => $dataClass) { + if (!$this->isClassNamePermitted($dataClass)) { + echo sprintf('ERROR: `%s` does not have FluentExtension installed. Continuing without it...', $dataClass) . PHP_EOL; + unset($classesWithFluent[$key]); + } + } + } else { + $dataClasses = static::getDirectSubclassesRecursivelyFor(DataObject::class); + $classesWithFluent = $this->filterPermittedClassesRecursively($dataClasses); + } + + foreach ($classesWithFluent as $classWithFluent) { + if (!$this->isClassNamePermitted($classWithFluent)) { + continue; + } + + $results = $this->doLocaliseClass($classWithFluent, $globalLocale, $limit, $publish); + foreach ($results as $key => $value) { + $total_results[$key] += $value; + } + + echo sprintf('Processing %s objects...', $classWithFluent) . PHP_EOL; + echo sprintf('└─ Localised %d of %d objects.', $results['localised'], $results['localisable']) . PHP_EOL; + if ($results['publishable']) { + echo sprintf('└─ Published %d of %d objects.', $results['published'], $results['publishable']) . PHP_EOL; + } + } + + echo PHP_EOL; + echo sprintf('Completed %d classes.', count($classesWithFluent)) . PHP_EOL; + echo sprintf('└─ Localised %d of %d objects in total.', $total_results['localised'], $total_results['localisable']) . PHP_EOL; + echo PHP_EOL; + + if ($total_results['publishable']) { + echo sprintf('└─ Published %d of %d objects in total.', $total_results['published'], $total_results['publishable']) . PHP_EOL; + echo PHP_EOL; + } + + if (!Director::is_cli()) { + echo ''; + } + } + + /** + * @param $className + * @param $globalLocale + * @param $limit + * @param $publish + * @return array{localisable: int, localised: int, publishable: int, published: int} + * @throws \SilverStripe\ORM\ValidationException + */ + protected function doLocaliseClass($className, $globalLocale, $limit, $publish): array + { + $dataObjectIDs = FluentState::singleton()->withState(static function (FluentState $state) use ($className, $limit): array { + $state->setLocale(null); + $dataObjects = $className::get()->sort('ID', 'ASC'); + + if ($limit > 0) { + $dataObjects = $dataObjects->limit($limit); + } + + return $dataObjects->column('ID'); + }); + + return FluentState::singleton()->withState( + static function (FluentState $state) use ($className, $globalLocale, $publish, $dataObjectIDs): array { + $state->setLocale($globalLocale->Locale); + $return = [ + 'localisable' => 0, + 'localised' => 0, + 'publishable' => 0, + 'published' => 0, + ]; + + foreach ($dataObjectIDs as $dataObjectID) { + /** @var DataObject|FluentExtension $dataObject */ + $dataObject = $className::get()->byID($dataObjectID); + $return['localisable'] += 1; + + if (!$dataObject->hasExtension(FluentVersionedExtension::class)) { + if ($dataObject->existsInLocale()) { + continue; + } + $dataObject->write(); + $return['localised'] += 1; + continue; + } + + // We have versioned data, so start tracking how many have been published + $return['publishable'] += 1; + + /** @var DataObject|Versioned|FluentVersionedExtension $dataObject */ + if ($dataObject->isDraftedInLocale()) { + continue; + } + $dataObject->writeToStage(Versioned::DRAFT); + + $return['localised'] += 1; + + if (!$publish) { + continue; + } + + // Check if the base record was published - if not then we don't need to publish + // as this would leak draft content, we only want to publish pages which were published + // before Fluent module was added + $dataObjectID = $dataObject->ID; + $isBaseRecordPublished = FluentState::singleton()->withState( + static function (FluentState $state) use ($className, $dataObjectID): bool { + $state->setLocale(null); + $page = $className::get_by_id($dataObjectID); + + if ($page === null) { + return false; + } + + return $page->isPublished(); + } + ); + + if (!$isBaseRecordPublished) { + continue; + } + + $dataObject->publishRecursive(); + $return['published'] += 1; + } + + return $return; + } + ); + } + + /** + * @param string $className + * @return array[] + * @throws \ReflectionException + */ + protected static function getDirectSubclassesRecursivelyFor(string $className): array + { + $directSubclasses = []; + foreach (ClassInfo::subclassesFor($className, false) as $subclassName) { + $actualParentClass = get_parent_class($subclassName); + if ($className === $actualParentClass) { + $directSubclasses[$subclassName] = static::getDirectSubclassesRecursivelyFor($subclassName); + } + } + + return $directSubclasses; + } + + /** + * @param array $classes + * @return array + */ + protected function filterPermittedClassesRecursively(array $classes): array + { + $permittedClasses = []; + foreach ($classes as $parentClassName => $subclassNames) { + if ($this->isClassNamePermitted($parentClassName)) { + $permittedClasses[] = $parentClassName; + // We will skip all subclasses since the ORM will automatically + // pull them in when this parent is referenced + continue; + } + + $permittedClasses = array_merge($permittedClasses, $this->filterPermittedClassesRecursively($subclassNames)); + } + + return $permittedClasses; + } + + /** + * @param string $className + * @return bool + */ + protected function isClassNamePermitted(string $className): bool + { + // Do a simple (inexpensive) text comparison against the exclusion list before we create an object + if (!$this->include_only_classes && is_array($this->exclude_classes) && in_array($className, $this->exclude_classes)) { + return false; + } + + /** @var DataObject $dataObject */ + $dataObject = singleton($className); + + // Now we'll do a full comparison against the exclusion list + // This important step will, for example, match (refuse) a BlogPost if Page is listed as excluded + if (is_array($this->exclude_classes)) { + foreach ($this->exclude_classes as $excluded_class) { + if ($dataObject instanceof $excluded_class) { + return false; + } + } + } + + return $dataObject->hasExtension(FluentExtension::class); + } +} diff --git a/src/Task/InitialPageLocalisationTask.php b/src/Task/InitialPageLocalisationTask.php index 393f8e37..d39bd7bc 100644 --- a/src/Task/InitialPageLocalisationTask.php +++ b/src/Task/InitialPageLocalisationTask.php @@ -3,14 +3,8 @@ namespace TractorCow\Fluent\Task; use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Control\HTTPRequest; -use SilverStripe\Dev\BuildTask; -use SilverStripe\Versioned\Versioned; -use TractorCow\Fluent\Extension\FluentSiteTreeExtension; -use TractorCow\Fluent\Model\Locale; -use TractorCow\Fluent\State\FluentState; -class InitialPageLocalisationTask extends BuildTask +class InitialPageLocalisationTask extends InitialDataObjectLocalisationTask { /** * @var string @@ -20,93 +14,34 @@ class InitialPageLocalisationTask extends BuildTask /** * @var string */ - protected $title = 'Initial page localisation'; + protected $title = 'Initial SiteTree localisation'; /** * @var string */ - protected $description = 'Intended for projects which already have some pages when Fluent module is added.' . - ' This dev task will localise / publish all pages in the default locale. Locale setup has to be done before running this task.' . - ' Pages which are not published will not be published, only localised. Pages which are already localised will be skipped.'; + protected $description = 'Intended for projects which already have some Pages when Fluent module is added.' . + ' This dev task will localise / publish all Pages in the default locale. Locale setup has to be done before running this task.' . + ' Pass limit=N to limit number of records to localise. Pass publish=1 to force publishing of localised Pages.' . + ' Regardless, Pages which were not already published will not be published, only localised. Pages which were already localised will always be skipped.'; /** - * @param HTTPRequest $request + * @var string[] */ - public function run($request) - { - $publish = (bool) $request->getVar('publish'); - $limit = (int) $request->getVar('limit'); - - $globalLocale = Locale::get() - ->filter(['IsGlobalDefault' => 1]) - ->sort('ID', 'ASC') - ->first(); - - if (!$globalLocale) { - echo 'Please set global locale first!' . PHP_EOL; - - return; - } - - $pageIds = FluentState::singleton()->withState(static function (FluentState $state) use ($limit): array { - $state->setLocale(null); - $pages = SiteTree::get()->sort('ID', 'ASC'); - - if ($limit > 0) { - $pages = $pages->limit($limit); - } - - return $pages->column('ID'); - }); - - $localised = FluentState::singleton()->withState( - static function (FluentState $state) use ($globalLocale, $pageIds, $publish): int { - $state->setLocale($globalLocale->Locale); - $localised = 0; - - foreach ($pageIds as $pageId) { - /** @var SiteTree|FluentSiteTreeExtension $page */ - $page = SiteTree::get()->byID($pageId); - - if ($page->isDraftedInLocale()) { - continue; - } - - $page->writeToStage(Versioned::DRAFT); - $localised += 1; + protected $include_only_classes = [ + SiteTree::class + ]; - if (!$publish) { - continue; - } - - // Check if the base record was published - if not then we don't need to publish - // as this would leak draft content, we only want to publish pages which were published - // before Fluent module was added - $pageId = $page->ID; - $isBaseRecordPublished = FluentState::singleton()->withState( - static function (FluentState $state) use ($pageId): bool { - $state->setLocale(null); - $page = SiteTree::get_by_id($pageId); - - if ($page === null) { - return false; - } - - return $page->isPublished(); - } - ); - - if (!$isBaseRecordPublished) { - continue; - } - - $page->publishRecursive(); - } - - return $localised; - } - ); + /** + * @var string[] + */ + protected $exclude_classes = []; - echo sprintf('Localised %d pages.', $localised) . PHP_EOL; + /** + * Soft dependency on CMS module + * @return bool + */ + function isEnabled(): bool + { + return class_exists(SiteTree::class) && parent::isEnabled(); } } diff --git a/tests/php/Middleware/InitStateMiddlewareTest.php b/tests/php/Middleware/InitStateMiddlewareTest.php index c2decf57..3989c35b 100644 --- a/tests/php/Middleware/InitStateMiddlewareTest.php +++ b/tests/php/Middleware/InitStateMiddlewareTest.php @@ -35,7 +35,8 @@ public function isFrontendProvider() ['/', [], true], ['foo', [], true], ['my-blog/my-post', [], true], - ['my-blog/my-post', ['CMSPreview' => 1], false], + // CMS preview is front-end, and if there's no localised copy the PreviewLink will be null + ['my-blog/my-post', ['CMSPreview' => 1], true], ]; } }