Silverstripe 4 Upgrade - Unversioned DataObjects in ModelAdmin lose their image objects - silverstripe

Been pulling my hair out over this for a day and exhausted my google foo. I have inherited a Silverstripe 3.4 site that we have upgraded to 4.4. But something odd has been going on with certain images after running MigrateFilesTask.
I think this is something to do with a file being attached to an unversioned objects that are accessed via ModelAdmin. But I have not been able to find a definitive solution.
Code for this object below. Problems experienced are under it.
<?php
use SilverStripe\Assets\Image;
use gorriecoe\Link\Models\Link;
use SilverStripe\Security\Member;
use SilverStripe\Control\Controller;
use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\FieldGroup;
use gorriecoe\LinkField\LinkField;
use SilverStripe\TagField\TagField;
use SilverStripe\ORM\DataObject;
use SilverStripe\SelectUpload\SelectUploadField;
class Person extends DataObject
{
private static $db = array(
'FirstName' => 'Varchar(128)',
'LastName' => 'Varchar(128)',
'Role' => 'Varchar(128)',
'DirectDialNumber' => 'Varchar(128)',
'Email' => 'Varchar(128)',
'CellphoneNumber' => 'Varchar(30)',
'DirectDial' => 'Varchar(30)',
'UrlSegment' => 'Varchar(255)',
'Blurb' => 'HTMLText',
'SortOrder' => 'Int'
);
private static $has_one = array(
'Image' => Image::class,
'Office' => 'Office',
'LinkedIn' => Link::class,
'Member' => Member::class
);
private static $many_many = array(
'Interests' => 'Section'
);
private static $belongs_many_many = array(
'ElementCollection' => 'ElementCollection'
);
static $sort_fields = array(
'FirstName' => 'First name',
'LastName' => 'Last name',
'Role' => 'Role'
);
private static $summary_fields = array(
'Name' => 'Name',
'Role' => 'Role',
'Office.Name' => 'Office'
);
private static $searchable_fields = array(
'FirstName',
'LastName',
'Role'
);
// For use with the ElementCollection
public static $templates = array(
'ElementPeople' => 'Default',
'ElementPeopleAlternative' => 'Alternative'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName( ['SortOrder', 'ElementCollection', 'FirstName', 'LastName', 'Interests'] );
$firstname = TextField::create('FirstName', 'First name');
$lastname = TextField::create('LastName', 'Last name');
$fields->addFieldsToTab('Root.Main', FieldGroup::create($firstname, $lastname)->setTitle('Name')->setName('Name'), 'Role');
$image = UploadField::create('Image', 'Photo');
$image->setFolderName('Uploads/People');
$image->setCanSelectFolder(false);
$fields->addFieldToTab('Root.Main', $image);
$linkedin = LinkField::create('LinkedIn', 'LinkedIn', $this);
$fields->addFieldToTab('Root.Main', $linkedin);
$interests = TagField::create(
'Interests',
'Interests Tags',
Section::get(),
$this->Interests()
)->setShouldLazyLoad(true)
->setCanCreate(false);
$fields->addFieldToTab('Root.Main', $interests);
return $fields;
}
public function onBeforeWrite()
{
$count = 1;
$this->UrlSegment = $this->generateURLSegment();
while (!$this->validURLSegment()) {
$this->UrlSegment = preg_replace('/-[0-9]+$/', null, $this->UrlSegment) . '-' . $count;
$count++;
}
parent::onBeforeWrite();
}
}
Problem #1 is after running MigrateFileTask, ALL existing images attached to instances of this class get moved from /assets/Uploads/People to /assets/.protected/Uploads/People. The confusing part here is that there is one other class called Company that is structurally near identical, yet images for that remain in /assets/Uploads/Companies as expected.
Problem #2 is if I create a new Person object and attach an image, that image is in Draft, sitting in /assets/.protected/Uploads/People with no method of actually publishing it. Meanwhile, if I do the same with a Company object, the image is still in Draft, but I can see it in the CMS.
Can someone offer some guidance on the above? At this point I'd be happy to just be able for images to be published when the DO is and I'll manually go through every single Person record and hit save myself just to get this upgrade over the line.

You should be able to fix this issue by adding the image to your DataObejct's owns property. Basically add this:
private static $owns = [
'Image'
];
Basically owns tells a DataObject which objects to publish when it is saved:
More info in the docs: https://docs.silverstripe.org/en/4/developer_guides/model/versioning/#defining-ownership-between-related-versioned-dataobjects

The cause of issue #1 was found. Leaving this here in case it helps someone in future:
The database table File has a row for every File and Folder in the system. This table has a column called "CanViewType". It exists in both Silverstripe 3 and 4.
For the particular Folder that was causing trouble during the Migration process, I found it was the only one with that column set to "OnlyTheseUsers". The rest were set to "Inherit". This was the state of the table before the upgrade.
I'm unsure how or by what mechanism that row is ever changed, but the solution to problem #1 was to manually change that field to "Inherit" before running FileMigrationTask.
Issue #2 persists, but it looks like there are two very different issues here.

OK. So sorted problem #2 finally (see other answer for solution to #1), but it's put a massive dent in our confidence in Silverstripe and sparked a meeting here.
Code for future readers:
In your unversioned DataObject, add this. In this case, my file object is called "Image". If you had more than one file to publish on save, you would have to add one of these IF blocks for each.
public function onAfterWrite()
{
if ($this->Image()->exists() && !$this->Image()->isPublished()) {
$this->Image()->doPublish();
}
parent::onAfterWrite();
}
SIDENOTE:
This Object/File relationship really is a strange design choice. Are there actually any situations where you would want to attach a file or image to a data object and NOT publish that file at the same time as you save/publish the object/page? Developers even need to explicitly define that on Versioned objects using $owns - which I'm happy to bet that most developers have to add more times than NOT. Which should really tell a us something is around wrong way.
Adding an image to a CMS system shouldn't be hard. It should take reading basic docs at the most. Not Googling, deep API doc dives (which don't answer much) or posting on StackOverlfow (where no one really knows the answer) over three days. It's an image. A core function of the product.
I've been working with SS since v2.4 and seen all the hard lessons learned to get to v4. But this appears to be a textbook case of the simple being over-engineered.

Related

How do you Filter CSV exports on silverstripe CMS?

I am trying to export a CSV of my data which is currently displayed in a section of my Silverstripe CMS as filtered by a particular date range . It works fine at the moment when exporting the entire contents but I would like to be able to filter the results that are exported so that it returns all results within a particular date range.
My Database has a column thats records the date created - in the format 'D-M-Y; H-M-S' which I think could be used to do the filtering but I cant figure out how to set up the search filter. I understand that if you use the searchable fields and then export, you only export the filtered search results so would assume thats the best way of doing it but can't figure out how to implement it.
Any suggestions would be greatly appreciated.
-- disclaimer - I would have liked to put this on the silverstripe forum but I am completely unable to sign up for some reason - I never receive the email confirmations. ---
<?php
namespace AffiliateProgram;
use SilverStripe\Forms\GridField\GridField;
use UndefinedOffset\SortableGridField\Forms\GridFieldSortableRows;
use SilverStripe\Security\Permission;
use SilverStripe\ORM\DataObject;
class MemberBonus extends DataObject
{
private static $db = [
'Amount' => 'Currency',
'Confirmed' => 'Boolean',
'Level' => 'Int',
'Percentage' => 'Int'
];
private static $has_one = [
'Member' => 'AffiliateProgram\Member',
'MemberPayment' => 'AffiliateProgram\MemberPayment',
'PaymentType' => 'AffiliateProgram\PaymentType',
'ProgramType' => 'AffiliateProgram\ProgramType'
];
private static $summary_fields = [
'Amount' => 'Amount (USD)',
'Member.Email' => 'Email',
'Level',
'MemberPayment.PaymentType.Symbol' => 'Recieved As',
'Percentage' => 'Percentage Bonus Applied',
'ProgramType.Name' => 'Program Type',
'MemberPayment.Created' => 'Payment Date',
'Confirmed' => 'Confirmed?',
'MemberPayment.ID' => 'Payment ID'
];
}
There is also a DateCreated column on the table.
You can add custom search fields to a ModelAdmin via getSearchContext(), and customise the query based on them with getList(). See this section of the SilverStripe documentation.
Here's an example of excluding results that have a CreatedAt value below the date provided in a search field (assuming your ModelAdmin only manages MemberBonus):
<?php
use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Forms\DatetimeField;
class MemberBonusAdmin extends ModelAdmin
{
...
public function getSearchContext()
{
$context = parent::getSearchContext();
$context->getFields()->push(new DatetimeField('q[CreatedAfter]', 'Created After'));
return $context;
}
public function getList()
{
$list = parent::getList();
$params = $this->getRequest()->requestVar('q');
if (!empty($params['CreatedAfter'])) {
$list = $list->exclude('CreatedAt:LessThan', $params['CreatedAfter']);
}
return $list;
}
}
To get a range working, you'd just need to add a CreatedBefore field and filter.

elfinder - copy hardlinks

We move alot of big files and this work flawlessly because this is done in a instance without copying the file. (just moving the pointer)
But sometimes we need acces to the same file from multiple locations in the filesystem, today we copy the file but this takes extremely long time and is storage consuming, this is expected since the file is copied to another place on disk. So we can't complain on this :)
But in our workflow we actually don't have to have multiple files, multiple pointers to the same file is enough. Therefore the hardlink model is perfect choice for us. This would be extremely usefull if it was possible to change the behavior on the copy feature in elfinder, Does anyone know if it is possible. ? :)
It is made possible by extending the elFinderVolumeLocalFileSystem class.
class elFinderVolumeMyLocalFileSystem extends elFinderVolumeLocalFileSystem
{
protected function _copy($source, $targetDir, $name) {
$target = $this->_joinPath($targetDir, $name);
if (! $ret = link($source, $target)) {
return parent::_copy($source, $targetDir, $name);
}
return $ret;
}
}
$opts = array(
'locale' => '',
'roots' => array(
array(
'driver' => 'MyLocalFileSystem',
'path' => '/path/to/files/',
'URL' => 'http://localhost/to/files/'
)
)
);
// run elFinder
$connector = new elFinderConnector(new elFinder($opts));
$connector->run();

Symfony2 and Sonata Admin Bundle - filter timestamp displayed as date

So i have a GUI for a simple database made with Symfony2 + Sonata Admin Bundle + Doctrine 2. That database will hold billions of rows so the dates are stored as timestamps (to save space) but in the GUI displayed as dates (ex: 2013/10/17 10:05:06). Everything works in the GUI except the filtering by dates. I tried all sorts of configurations in the class that extends the Admin, method configureDatagridFilters(). I cannot make it to work... can you help me?
I found a work around that i think it can be useful in many other situations as well:
So in the class that extends Admin.php we have the method configureDatagridFilters(). There one can add an input like
->add('startDateFrom', 'doctrine_orm_callback', array(
'label' => 'Start Date from',
'callback' => function($queryBuilder, $alias, $field, $value) {
if (!$value['value']) {
return;
}
$inputValue = $this->dateStringToTimestamp($value['value']);
$queryBuilder->andWhere("{$alias}.startDate >= :startDateFrom");
$queryBuilder->setParameter('startDateFrom', $inputValue);
return true;
},
'field_type' => 'text'
), null, array('attr' => array('class' => 'calendar',
)))
As you can see, in this way we can manipulate the field value as we wish. I hope it will help others too :)

Using token as data selector

I created the following token; however, when I try to use site:coupons as a data selector in a loop action
It does not appear in data selection browser. Note that it does appear as replacement pattern when i use for example "Show a message on the site" action.
I spent lot of time searching in the internet and rules' token' issue queue, i tried to read the source codes of core token , token and rules as well. I also found some information too like data selector are no tokens! or rules only works with entities!
So far i couldn't get this to work no matter hard i tried. My data is not entity. Is there anyway to integrate it with rules?
I couldn't find any official documentation on this so i created an issue with hope that some of the rule's experts can help me out.
Note : if i replace site with coupon-link in the following code, it won't even appear as replacement pattern in rules. but it works fine as token anywhere else but in rules
Thanks in advance
<?php
/**
* Implements hook_token_info().
*/
function coupon_link_token_info() {
$types['coupon-link'] = array(
'name' => t("Coupon link coupon info"),
'description' => t("Info about linked coupon via url."),
);
// Andy Pangus specific tokens.
$tokens['site']['coupon-code'] = array(
'name' => t("Coupon Link Coupon Code"),
'description' => t("The code of the coupon entered via url."),
);
$tokens['site']['coupon'] = array(
'name' => t("Coupon Link Coupon"),
'description' => t("The coupon entered via url."),
'type' => 'commerce_coupon'
);
$tokens['site']['coupons'] = array(
'name' => t("Coupon Link List Coupons"),
'description' => t("The coupons entered via url."),
'type' => 'array'
);
return array(
'types' => $types,
'tokens' => $tokens,
);
}
/**
* Implements hook_tokens().
*
* #ingroup token_example
*/
function coupon_link_tokens($type, $tokens, array $data = array(), array $options = array()) {
$replacements = array();
$sanitize = !empty($options['sanitize']);
// Text format tokens.
if ($type == 'site' && __coupon_link_get_coupon_code()) {
//$format = $data['format'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'coupon-code':
// Since {filter_format}.format is an integer and not user-entered
// text, it does not need to ever be sanitized.
$replacements[$original] = $sanitize ? filter_xss(__coupon_link_get_coupon_code()) : __coupon_link_get_coupon_code();
break;
case 'coupon':
// Since the format name is user-entered text, santize when requested.
$replacements[$original] = __coupon_link_get_coupon(__coupon_link_get_coupon_code());
break;
case 'coupons':
// Since the format name is user-entered text, santize when requested.
$replacements[$original] = array(__coupon_link_get_coupon(__coupon_link_get_coupon_code()));
break;
}
}
}
return $replacements;
}
?>
A few things.
Tokens are formatted as [type:token] as explained on the hook_token_info api page. For your example, it would be [coupon-link:coupon]. I'm not sure why you're appending your tokens to the site array, as your custom coupon token probably has nothing to do with sitewide tokens like *site_url* or *site_name*.
Because the types are machine names, you should change it to coupon_link as machine names with dashes are not Drupal standard.
If you truly get lost, I suggest also looking at the token example from the examples module.

Silverstripe crashes when trying to save a date

I'm trying to create a very simple performance class that extends dataobject and has a date for one of the fields but for some reason if I fill it out when I press 'Add' it just hangs. I've checked in the Chrome inspector and it fires off the Ajax request but the request never returns anything, I've even tried leaving it for a few minutes and still nothing returns and nothing gets puts in the error log either.
If I leave the date blank and just fill out the text field it works fine, here is the code I'm using.
class Performance extends DataObject {
static $db = array(
'Title' => 'Varchar(255)',
'StartDate' => 'Date',
);
static $summary_fields = array(
'Title' => 'Title',
'Starts' => 'StartDate',
);
static $has_one = array(
'Production' => 'ProductionPage'
);
}
What's really weird is if I grab the ArticlePage class from the extending Silverstripe tutorial it works fine, it's just if I try and do it with a DataObject rather than a page that I run into this.
Would really appreciate any help with this, I've been struggling with it for hours now.
Try the Legacydatetimefields module: http://www.silverstripe.org/legacydatetimefields-module/
There was a change of how Silverstripe handles date and time in the latest version (2.4) which means some older code dealing with date and time doesn't work.
Hope this helps.

Resources