1
<?php
2

3
namespace SilverStripe\VersionedAdmin;
4

5
use SilverStripe\Admin\ModelAdmin;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Core\ClassInfo;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Forms\DropdownField;
10
use SilverStripe\Forms\FieldList;
11
use SilverStripe\Forms\Form;
12
use SilverStripe\Forms\GridField\GridField;
13
use SilverStripe\Forms\GridField\GridField_ActionMenu;
14
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
15
use SilverStripe\Forms\GridField\GridFieldDataColumns;
16
use SilverStripe\Forms\GridField\GridFieldDetailForm;
17
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
18
use SilverStripe\Forms\GridField\GridFieldViewButton;
19
use SilverStripe\ORM\ArrayList;
20
use SilverStripe\ORM\DataObject;
21
use SilverStripe\ORM\FieldType\DBDatetime;
22
use SilverStripe\Versioned\GridFieldRestoreAction;
23
use SilverStripe\Versioned\Versioned;
24
use SilverStripe\Versioned\VersionedGridFieldState\VersionedGridFieldState;
25
use SilverStripe\VersionedAdmin\Interfaces\ArchiveViewProvider;
26
use SilverStripe\View\ArrayData;
27

28
/**
29
 * Archive admin is a section of the CMS that displays archived records
30
 * from versioned objects and allows for users to restore them.
31
 *
32
 * Shows tabs for any implementors of {@link ArchiveViewProvider} and
33
 * display any other versioned objects in a dropdown
34
 */
35
class ArchiveAdmin extends ModelAdmin
36
{
37
    private static $url_segment = 'archive';
38

39
    private static $menu_title = 'Archives';
40

41
    private static $menu_icon_class = 'font-icon-box';
42

43
    public $showSearchForm = false;
44

45 0
    protected function init()
46
    {
47 0
        parent::init();
48

49
        // Set the default model class to SiteTree, as long as silverstripe/cms is installed
50
        // This is done otherwise File will be the default set in ModelAdmin::init() which is basically random
51 0
        $class = 'SilverStripe\\CMS\\Model\\SiteTree';
52 0
        if (!$this->getRequest()->param('ModelClass') && !$this->request->getVar('others') && class_exists($class)) {
53 0
            $this->modelClass = $class;
54
        }
55
    }
56

57
    /**
58
     * Produces an edit form with relevant prioritised tabs for Pages, Blocks and Files
59
     *
60
     * @param int|null $id
61
     * @param FieldList|null $fields
62
     * @return Form A Form object with one tab per {@link \SilverStripe\Forms\GridField\GridField}
63
     */
64 0
    public function getEditForm($id = null, $fields = null)
65
    {
66 0
        $fields = FieldList::create();
67 0
        $modelClass = $this->request->getVar('others') ? 'others' : $this->modelClass;
68 0
        $classInst = Injector::inst()->get($this->modelClass);
69

70 0
        if (ClassInfo::hasMethod($classInst, 'getArchiveField')) {
71 0
            $listField = $classInst->getArchiveField();
72 0
            $fields->push($listField);
73
        } else {
74 0
            $otherVersionedObjects = $this->getVersionedModels('other');
75 0
            $modelSelectField = $this->getOtherModelSelectorField($modelClass);
76 0
            $fields->push($modelSelectField);
77

78
            // If a valid other model name is passed via a request param
79
            // then show a gridfield with archived records
80 0
            if (array_search($modelClass, $otherVersionedObjects)) {
81 0
                $listField = $this->createArchiveGridField('Others', $modelClass);
82

83 0
                $listColumns = $listField->getConfig()->getComponentByType(GridFieldDataColumns::class);
84 0
                $listColumns->setDisplayFields([
85 0
                    'Title' => _t(__CLASS__ . '.COLUMN_TITLE', 'Title'),
86 0
                    'allVersions.first.LastEdited' => _t(__CLASS__ . '.COLUMN_DATEARCHIVED', 'Date Archived'),
87 0
                    'allVersions.first.Author.Name' => _t(__CLASS__ . '.COLUMN_ARCHIVEDBY', 'Archived By'),
88
                ]);
89 0
                $listColumns->setFieldFormatting([
90 0
                    'allVersions.first.LastEdited' => function ($val, $item) {
91 0
                        return DBDatetime::create_field('Datetime', $val)->Ago();
92 0
                    },
93
                ]);
94

95 0
                $fields->push($listField);
96
            }
97
        }
98

99 0
        $form = Form::create(
100 0
            $this,
101 0
            'EditForm',
102
            $fields,
103 0
            FieldList::create()
104 0
        )->setHTMLID('Form_EditForm');
105 0
        $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
106 0
        $form->setAttribute('data-pjax-fragment', 'CurrentForm');
107 0
        $form->addExtraClass(
108
            'ArchiveAdmin discardchanges cms-edit-form cms-panel-padded center flexbox-area-grow '.
109 0
            $this->BaseCSSClasses()
110
        );
111 0
        $form->setFormAction(Controller::join_links(
112 0
            $this->Link($this->sanitiseClassName($this->modelClass)),
113 0
            'EditForm'
114
        ));
115

116 0
        $this->extend('updateEditForm', $form);
117

118 0
        return $form;
119
    }
120

121
    /**
122
     * Create a gridfield which displays archived objects
123
     *
124
     * @param string $title
125
     * @param string $class
126
     * @return GridField
127
     */
128 0
    public static function createArchiveGridField($title, $class)
129
    {
130 0
        $config = GridFieldConfig_Base::create();
131 0
        $config->removeComponentsByType(VersionedGridFieldState::class);
132 0
        $config->removeComponentsByType(GridFieldFilterHeader::class);
133 0
        $config->addComponent(new GridFieldDetailForm);
134 0
        $config->addComponent(new GridFieldViewButton);
135 0
        $config->addComponent(new GridFieldRestoreAction);
136 0
        $config->addComponent(new GridField_ActionMenu);
137

138 0
        $list = singleton($class)->get();
139 0
        $baseTable = singleton($list->dataClass())->baseTable();
140 0
        $liveTable = $baseTable . '_Live';
141

142
        $list = $list
143 0
            ->setDataQueryParam('Versioned.mode', 'latest_versions');
144
        // Join a temporary alias BaseTable_Draft, renaming this on execution to BaseTable
145
        // See Versioned::augmentSQL() For reference on this alias
146 0
        $draftTable = $baseTable . '_Draft';
147
        $list = $list
148 0
            ->leftJoin(
149 0
                $draftTable,
150 0
                "\"{$baseTable}\".\"ID\" = \"{$draftTable}\".\"ID\""
151
            );
152

153 0
        $list = $list->leftJoin(
154 0
            $liveTable,
155 0
            "\"{$baseTable}\".\"ID\" = \"{$liveTable}\".\"ID\""
156
        );
157

158 0
        $list = $list->where("\"{$draftTable}\".\"ID\" IS NULL");
159 0
        $list = $list->sort('LastEdited DESC');
160

161 0
        $field = GridField::create(
162 0
            $title,
163 0
            false,
164
            $list,
165
            $config
166
        );
167 0
        $field->setModelClass($class);
168

169 0
        return $field;
170
    }
171

172
    /**
173
     * Returns versioned objects, can be filtered for 'main' (has a tab)
174
     * or 'other' and is exposed through the 'Others' tab, returns all
175
     * by default
176
     *
177
     * @param string|null $filter Filter by 'main' or 'other'
178
     * @param boolean $forDisplay Include titles as values in the returned array
179
     * @return array
180
     */
181 1
    public function getVersionedModels($filter = null, $forDisplay = false)
182
    {
183
        // Get dataobjects with staged versioning
184 1
        $versionedClasses = array_filter(
185 1
            ClassInfo::subclassesFor(DataObject::class),
186 1
            function ($class) {
187
                return (
188 1
                    DataObject::has_extension($class, Versioned::class) &&
189 1
                    DataObject::singleton($class)->hasStages()
190
                );
191
            }
192
        );
193

194 1
        $archiveProviders = ClassInfo::implementorsOf(ArchiveViewProvider::class);
195

196 1
        $disabledHandledClasses = [];
197

198
        // Get the classes that are declared as handled by the disabled providers
199 1
        foreach ($archiveProviders as $provider) {
200 1
            if (!Injector::inst()->get($provider)->isArchiveFieldEnabled()) {
201 1
                $disabledProviderClass = Injector::inst()->get($provider)->getArchiveFieldClass();
202 1
                $disabledHandledClasses[] = $disabledProviderClass;
203

204 1
                $disabledHandledClasses = array_merge(
205 1
                    $disabledHandledClasses,
206 1
                    array_keys(ClassInfo::subclassesFor($disabledProviderClass))
207
                );
208
            }
209
        }
210

211
        // Remove any subclasses that would also be handled by those disabled providers
212 1
        $versionedClasses = array_diff_key($versionedClasses, array_flip($disabledHandledClasses));
213

214
        // If there is a valid filter passed
215 1
        if ($filter && in_array($filter, ['main', 'other'])) {
216 1
            $archiveProviderClasses = [];
217

218
            // Get the classes that are decalred as handled by ArchiveViewProviders
219 1
            foreach ($archiveProviders as $provider) {
220 1
                $archiveProviderClass = Injector::inst()->get($provider)->getArchiveFieldClass();
221 1
                $archiveProviderClasses[] = $archiveProviderClass;
222
            }
223

224 1
            switch ($filter) {
225 0
                case 'other':
226 1
                    $handledClasses = [];
227
                    // Get any subclasses that would also be handled by those providers
228 1
                    foreach ($archiveProviderClasses as $archiveProviderClass) {
229 1
                        $handledClasses = array_merge(
230 1
                            $handledClasses,
231 1
                            array_keys(ClassInfo::subclassesFor($archiveProviderClass))
232
                        );
233
                    }
234 1
                    $versionedClasses = array_filter(
235 1
                        $versionedClasses,
236 1
                        function ($class) use ($handledClasses) {
237 1
                            return !in_array(strtolower($class), $handledClasses);
238
                        }
239
                    );
240 1
                    break;
241
                default: // 'main'
242 1
                    $versionedClasses = array_filter(
243 1
                        $versionedClasses,
244 1
                        function ($class) use ($archiveProviderClasses) {
245 1
                            return in_array($class, $archiveProviderClasses);
246
                        }
247
                    );
248 1
                    break;
249
            }
250
        }
251

252
        // Formats array as [$className => i18n_plural_name]
253 1
        if ($forDisplay) {
254 0
            $versionedClasses = array_flip($versionedClasses);
255

256 0
            foreach (array_keys($versionedClasses) as $className) {
257 0
                $versionedClasses[$className] = ucfirst($className::singleton()->i18n_plural_name());
258
            }
259
        }
260

261 1
        return $versionedClasses;
262
    }
263

264
    /**
265
     * Creates a dropdown field that displays other archived models
266
     *
267
     * @param string $currentModel The model that is currently selected
268
     * @return DropdownField
269
     */
270 0
    public function getOtherModelSelectorField($currentModel = '')
271
    {
272 0
        $otherVersionedObjects = $this->getVersionedModels('other', true);
273

274 0
        $modelSelectField = DropdownField::create(
275 0
            'OtherDropdown',
276 0
            _t(__CLASS__ . '.SELECT_TYPE', 'Select a content type'),
277
            $otherVersionedObjects,
278
            $currentModel
279
        );
280 0
        $modelSelectField->setAttribute(
281 0
            'data-others-archive-url',
282 0
            $this->Link('/')
283
        );
284 0
        $modelSelectField->addExtraClass('other-model-selector');
285 0
        $modelSelectField->setEmptyString(_t(__CLASS__ . '.SELECT_EMPTY', 'Select…'));
286 0
        $modelSelectField->setHasEmptyDefault(true);
287

288 0
        return $modelSelectField;
289
    }
290

291
    /**
292
     * Use 'Archives' as the top title rather than the model title
293
     *
294
     * @param bool $unlinked
295
     * @return ArrayList
296
     */
297 0
    public function Breadcrumbs($unlinked = false)
298
    {
299 0
        $items = parent::Breadcrumbs($unlinked);
300

301 0
        $items[0]->Title = $this->menu_title();
302

303 0
        return $items;
304
    }
305

306
    /**
307
     * Archive admin needs some extra logic for whether an archive tab should be shown
308
     *
309
     * @return array Map of class name to an array of 'title' (see {@link $managed_models})
310
     */
311 0
    public function getManagedModels()
312
    {
313 0
        $models = $this->getVersionedModels();
314

315
        // Normalize models to have their model class in array key and all names as the value are uppercased
316 0
        foreach ($models as $k => $v) {
317 0
            $archivedModels[$v] = array('title' => ucfirst(singleton($v)->i18n_plural_name()));
318 0
            unset($archivedModels[$k]);
319
        }
320

321 0
        return $archivedModels;
322
    }
323

324
    /**
325
     * Add the special 'Others' tab
326
     *
327
     * @return ArrayList An ArrayList of all managed models to build the tabs for this ModelAdmin
328
     */
329 0
    public function getManagedModelTabs()
330
    {
331 0
        $forms = ArrayList::create();
332 0
        $mainModels = $this->getVersionedModels('main', true);
333

334
        // Display order should be Pages > Blocks > Files > Other
335
        // Pages, Blocks, Files are treated specially and have extensions defined in _config/archive-admin.yml
336 0
        $order = ['Pages', 'Blocks', 'Files'];
337 0
        uasort($mainModels, function ($a, $b) use ($order) {
338 0
            return array_search($a, $order) < array_search($b, $order) ? -1 : 1;
339 0
        });
340

341
        foreach ($mainModels as $class => $title) {
342
            $classInst = Injector::inst()->get($class);
343
            if (ClassInfo::hasMethod($classInst, 'isArchiveFieldEnabled')
344
                && $classInst->isArchiveFieldEnabled()
345
            ) {
346
                $forms->push(ArrayData::create([
347
                    'Title' => $title,
348
                    'ClassName' => $class,
349
                    'Link' => $this->Link($this->sanitiseClassName($class)),
350
                    'LinkOrCurrent' => ($class === $this->modelClass) ? 'current' : 'link'
351
                ]));
352
            }
353
        }
354

355
        $otherModels = $this->getVersionedModels('other', true);
356
        if ($otherModels) {
357
            $isOtherActive = (
358
                $this->request->getVar('others') !== null ||
359
                array_key_exists($this->modelClass, $otherModels)
360
            );
361
            $forms->push(ArrayData::create([
362
                'Title' => _t(__CLASS__ . '.TAB_OTHERS', 'Other'),
363
                'ClassName' => 'Others',
364
                'Link' => $this->Link('?others=1'),
365
                'LinkOrCurrent' => ($isOtherActive ? 'current' : 'link')
366
            ]));
367
        }
368

369
        $forms->first()->LinkOrCurrent = 'link';
370

371
        return $forms;
372
    }
373
}

Read our documentation on viewing source code .

Loading