Automatically populating dataobjectset with contents of assets subfolder in Silverstripe - silverstripe

I'm currently working on a Silverstripe 3.1 website that has dozens of random header images.
I can easily setup a "HeaderImage" databobjectset, but manually adding every image via the CMS would be a tedious headache.
Is there a simple way to have a dataobjectset automatically populated by the contents of a folder.
For example every image file in /assets/header-images/ automatically becomes a "HeaderImage" object. I want to be able to easily add or remove images.
Any ideas would be appreciated.

some details about the proposed solutions.
1) Like #3dgoo mentioned, using the GridFieldBulkEditingTools module. Download the latest master of best via composer "colymba/gridfield-bulk-editing-tools": "dev-master". This will let you upload a bunch of images and will create a DataObject for each one. Use the Bulk upload button. Here is how to have it set up in ModelAdmin:
class HeaderAdmin extends ModelAdmin
{
private static $managed_models = array('HeaderImage');
private static $url_segment = 'header-admin';
private static $menu_title = 'Header admin';
public function getEditForm($id = null, $fields = null)
{
$form = parent::getEditForm($id, $fields);
$gridField = $form->Fields()->fieldByName($this->sanitiseClassName('HeaderImage'));
if ( $gridField )
{
$gridField->getConfig()->addComponent(new GridFieldBulkImageUpload());
}
return $form;
}
}
2) Another solution, which would require a lot more work, is create a BuildTask and sort out the logic in run():
class ImportHeaderImagesTask extends BuildTask
{
protected $title = 'Import Header Images';
protected $description = 'Import Header Images......';
/**
* Check that the user has appropriate permissions to execute this task
*/
public function init()
{
if( !Director::is_cli() && !Director::isDev() && !Permission::check('ADMIN') )
{
return Security::permissionFailure();
}
parent::init();
}
/**
* Do some stuff
*/
public function run($request)
{
// this is where files are uploaded manually
$TempFTPFolder = ASSETS_PATH . '/FTP';
// This is the folder where files will be moved
$LiveFolderPath = 'assets/path/to/final/live/folder/';
$LiveFolder = DataObject::get_one('File', "Filename = '$LiveFolderPath'");
if ( file_exists( $TempFTPFolder ) && $LiveFolder->ID ) // if the FTP upload folder exist and the destination live folder exist
{
$FTPList = scandir( $TempFTPFolder ); // get the FTP folder content
foreach ($FTPList as $FileFolder)
{
$FTPFile = $TempFTPFolder . '/' . $FileFolder;
if ( is_file( $FTPFile ) ) // process files only
{
// Create File object for the live version
$NewFile = new File();
$NewFile->setParentID( $LiveFolder->ID );
$NewFile->setName( $FileFolder );
// get target name/path
$RenameTarget = $NewFile->getFullPath();
if ( $RenameTarget )
{
$moved = false;
try {
$moved = rename( $FTPFile, $RenameTarget ); // move the FTP file to the live folder
} catch (Exception $e) {}
if ( $moved )
{
$NewFile->write();
// create DataObject and add image relation
$HeaderImage = HeaderImage::create();
$HeaderImage->ImageID = $NewFile->ID;
$HeaderImage->write();
}
}
}
}
}
}
}
You can run this tasks via the dev/ url or via the command line or a CRON job. Note that I adapted the run() logic from something I've done a while ago, so not guaranteed it will work by just copy/pasting.

Related

Silverstripe 3.2: How to make a custom action button in the CMS to create a new Dataobject and populate it from another one

I'm searching for a way to create a custom action button which allows me to make a new DataObject with pre-filled content from another DataObject. As a simple example: When I have an email and click the "answer"-button in my email-client, I get a new window with pre-filled content from the email before. I need exactly this functionality for my button. This button should appear next to each DataObject in the GridField.
So I know how to make a button and add it to my GridField (--> https://docs.silverstripe.org/en/3.2/developer_guides/forms/how_tos/create_a_gridfield_actionprovider/) and I know how to go to a new DataObject:
Controller::curr()->redirect($gridField->Link('item/new'));
I also found out that there is a duplicate function for DataObjects:
public function duplicate($doWrite = true) {
$className = $this->class;
$clone = new $className( $this->toMap(), false, $this->model );
$clone->ID = 0;
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
if($doWrite) {
$clone->write();
$this->duplicateManyManyRelations($this, $clone);
}
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
return $clone;
}
Perhaps it's easier than I think but at the moment I just don't get how to rewrite this to get what I need. Can somebody give me a hint?
That's for sure not the cleanest solution but I think it should do the trick.
At first let's create the custom gridfield action. Here we will save all accessible records in a session and add a query string to the url so that we'll know which object we want to "clone"
public function getColumnContent($gridField, $record, $columnName) {
if(!$record->canEdit()) return;
$field = GridField_FormAction::create(
$gridField,
'clone'.$record->ID,
'Clone',
'clone',
array('RecordID' => $record->ID)
);
$values = Session::get('ClonedData');
$data = $record->data()->toMap();
if($arr = $values) {
$arr[$record->ID] = $data;
} else {
$arr = array(
$record->ID => $data
);
}
Session::set('ClonedData', $arr);
return $field->Field();
}
public function getActions($gridField) {
return array('clone');
}
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
if($actionName == 'clone') {
$id = $arguments['RecordID'];
Controller::curr()->redirect($gridField->Link("item/new/?cloneID=$id"));
}
}
after adding this new component to our gridfield,
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
we'll need to bring the data into the new form. To do so, add this code directly above "return $fields" on your getCMSFields function so it will be executed every time we'll open this kind of object.
$values = Session::get('ClonedData');
if($values) {
Session::clear('ClonedData');
$json = json_encode($values);
$fields->push(LiteralField::create('ClonedData', "<div id='cloned-data' style='display:none;'>$json</div>"));
}
At the end we need to bring the content back into the fields. We'll do that with a little bit of javascript so at first you need to create a new script.js file and include it in the ss backend (or just use an existing one).
(function($) {
$('#cloned-data').entwine({
onmatch: function() {
var data = JSON.parse($(this).text()),
id = getParameterByName('cloneID');
if(id && data) {
var obj = data[id];
if(obj) {
$.each(obj, function(i, val) {
$('[name=' + i + ']').val(val);
});
}
}
}
});
// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript#answer-901144
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
})(jQuery);
And that's it ... quite tricky. Hope it will solve your problem.

What is the best way to create a search facility for static content in Symfony?

I've built a static website using Symfony 2.6, it has been translated into 8 different languages and includes several forms.
It now requires a search facility, what is the best way to achieve this?
Search facility can be gained using:
Special database requests, in mysql SELECT * FROM article WHERE article.body LIKE '%searched_query%'
Full text search can be achieved using tools like:
Sphinx Search
Apache Solr
But in both cases you should save your content in database or other files.
In your case as workaround I suggest to crawl your own site and return links from sites where you found the searched text sort of this stuff
use Symfony\Component\DomCrawler\Crawler;
class InternalCrawler {
private $crawler;
private $textToSearch;
private $matchedUrls;
public function __construct($textToSearch)
{
$this->textToSearch = $textToSearch;
}
protected function requestUrl($url)
{
//curl the url to crawl
//...
return $html;
}
protected function getUrlsToCrawl()
{
return array(
'url-to-homepage',
'url-to-an-article-page',
...
);
}
protected function match($url, $html)
{
$this->crawler = new Crawler($html);
$textExists = $this->crawler->filter("html:contains('{$this->textToSearch}')")->count();
if ($textExists) {
$this->matchedUrls[] = $url;
}
}
public function getMatchedUrls()
{
foreach ($this->getUrlsToCrawl() as $url) {
$html = $this->requestUrl($url);
$this->match($url, $html);
}
return $this->matchedUrls;
}
}
As a result you will have the list of urls that matched your searched text.

Prestashop menu tab, that opens an iframe(Backoffice)

I'm working on a project for creating a prestashop module that creates a custom tab at the back office and by pressing it, it opens an iframe. I have created the tab but at the menu bar at the backoffice. But i dont know how open an iframe by pressing it.
Can you help me out please?
This is my module's code:
<?php
if (!defined('_PS_VERSION_'))
exit;
class Mytab extends Module
{
// PLIROFORIES TOY AYTHOR
public function __construct()
{
$this->name = 'Mytab';
$this->tab = 'Administration';
$this->version = 1.5;
$this->author = 'Sergio Kagiema';
$this->need_instance = 0;
//$this->tabParentName = 'AdminTools';
parent::__construct();
$this->displayName = $this->l('My Tab');
$this->description = $this->l('Module makes a tab at BackEnd');
$this->confirmUninstall = $this->l('Are you sure you want to uninstall?');
if (!Configuration::get('My Tab'))
$this->warning = $this->l('No name provided');
} //END OF PLIROFORIES TOY AYTHOR
//INSTALL TOY MODULE
public function install()
{
$parent_tab = new Tab();
foreach (Language::getLanguages(true) as $lang)
$parent_tab->name [$lang['id_lang']] = 'Tab';
$parent_tab->class_name = 'Tab';
$parent_tab->id_parent = 0;
$parent_tab->module = $this->name;
$parent_tab->add();
if (!parent::install()
|| !$this->installModuleTab('MyTabsController', array((int)(Configuration::get('PS_LANG_DEFAULT'))=>'My Tab'), $parent_tab->id)
)
return false;
return true;
}
//UNISTALL TOY MODULE
public function uninstall()
{
if (!parent::uninstall()
|| !$this->uninstallModuleTab('MyTab')
|| !$this->uninstallModuleTab('MyTabsController'))
return false;
return true;
}
private function installModuleTab($tabClass, $tabName, $idTabParent)
{
$idTab = Tab::getIdFromClassName($idTabParent);
$idTab = $idTabParent;
$pass = true ;
#copy(_PS_MODULE_DIR_.$this->name.'/logo.gif', _PS_IMG_DIR_.'t/'.$tabClass.'.gif');
$tab = new Tab();
$tab->name = $tabName;
$tab->class_name = $tabClass;
$tab->module = $this->name;
$tab->id_parent = $idTab;
$pass = $tab->save();
return($pass);
}
private function uninstallModuleTab($tabClass)
{
$pass = true ;
#unlink(_PS_IMG_DIR_.'t/'.$tabClass.'.gif');
$idTab = Tab::getIdFromClassName($tabClass);
if($idTab != 0)
{
$tab = new Tab($idTab);
$pass = $tab->delete();
}
return($pass);
}
}
?>
This is my controller's code at my contoroller/admin file:
<?php
class AffiliatesTabsController extends AdminController
{
public function init()
{
parent::init();
}
/**
* Assign template vars related to page content
* #see FrontController::initContent()
*/
public function initContent()
{ parent::initContent();
$this->setTemplate(_PS_THEME_DIR_.'/MyTab.tpl');
//$smarty = $this->context->smarty;
//$smarty->assign('test', 'test1');
// include(dirname(__FILE__).'/jQ.tpl');
}
}
?>
Please help me! Thanks!
I'm searching for similar things today and find out.
Controller:
class AffiliatesTabsController extends ModuleAdminController
{
public function initContent()
{
parent::initContent();
$this->setTemplate('MyTab.tpl');
}
}
Then move MyTab.tpl into
prestashop\modules\mytab\views\templates\admin\affiliatestabs\
MyTab.tpl:
<iframe src="webpage url">
You are trying to set an admin template for frontoffice use, that won't work.
This solution here works for me:
How to create a new page in prestashop admin panel?
Edit: additions after comments below:
After you created the the AdminAffiliatesController.php file in controllers\admin folder you create the template file here:
admin\themes\default\template\controllers\adminaffiliates\content.tpl
In that content.tpl file you can create your iframe or whatever.
Every output that you create in the controller or default values that you wan't to use or display in your template do you need to assign in the controller like this:
$title_of_page = "Welcome at this page!";
$smarty->assign('title_of_page', 'title_of_page');
In your template:
<h1>{$title_of_page}</h1>

Silverstripe Custom Validator on Uploadfield

I am using foresight.js to load hi-res images for retina devices. Foresight attempts to replace lo-res images with 2x-pixel density images. Since foresight attempts to replace lo-res images before the page has rendered, it is not possible for me to use the GD image resizing methods in the template for my resized images. So, I am allowing the SS 3.1 cms user to upload one large image and having the system re-size it after upload - leaving a 1x and 2x image in the assets folder.
My question is how can I set a custom validation error message if the cms user does not upload a large enough image?
Here is the code that resizes the image on upload.
class ResampledImage extends Image {
static $default_lores_x = 250;
static $default_lores_y = 250;
static $default_hires_x = 500;
static $default_hires_y = 500;
static $default_assets_dir = 'Uploads';
static $hires_flag = '2x';
function getLoResX() {
return ( static::$lores_x ) ? static::$lores_x : self::$default_lores_x;
}
function getLoResY() {
return ( static::$lores_y ) ? static::$lores_y : self::$default_lores_y;
}
function getHiResX() {
return ( static::$hires_x ) ? static::$hires_x : self::$default_hires_x;
}
function getHiResY() {
return ( static::$hires_y ) ? static::$hires_y : self::$default_hires_y;
}
function getAssetsDir() {
return ( static::$assets_dir ) ? static::$assets_dir : self::$default_assets_dir;
}
function onAfterUpload() {
$this->createResampledImages();
}
function onAfterWrite() {
$this->createResampledImages();
}
function createResampledImages() {
$extension = strtolower($this->getExtension());
if( $this->getHeight() >= $this->getHiResX() || $this->getWidth() >= $this->getHiResY() ) {
$original = $this->getFullPath();
$resampled = $original. '.tmp.'. $extension;
$orig_title = $this->getTitle();
$path_to_hires = Director::baseFolder() . '/' . ASSETS_DIR . '/' . $this->getAssetsDir();
$hires = $path_to_hires . '/' . $orig_title . self::$hires_flag . '.' . $extension;
$gd_lg = new GD($original);
$gd_sm = new GD($original);
if ( $gd_lg->hasImageResource() ) {
$gd_lg = $gd_lg->resizeRatio($this->getHiResX(), $this->getHiResY());
if ( $gd_lg )
$gd_lg->writeTo($hires);
}
if($gd_sm->hasImageResource()) {
$gd_sm = $gd_sm->resizeRatio($this->getLoResX(), $this->getLoResY());
if($gd_sm) {
$gd_sm->writeTo($resampled);
unlink($original);
rename($resampled, $original);
}
}
}
}
Looking at UploadField::setFileEditValidator() it appears that I can designate a method on my extended Image class to use as a Validator so that I can check for $this->getWidth() and $this->getHeight() and return an error if they are not large enough.
Is this possible?
I tried adding the following method to ResampledImage, but this was unsuccessful:
function MyValidator() {
$valid = true;
if ( $this->getHeight() < $this->getHiResX() || $this->getWidth() < $this->getHiResY() ) {
$this->validationError("Thumbnail",'Please upload a larger image');
$valid = false;
}
return $valid;
}
I think the fileEditValidator is acutally used after the image has been uploaded and is for the EditForm when displayed/edited.
Seems that what you are looking for is validate the Upload. You can set a custom Upload_Validator with setValidator($validator) on your UploadField.
So what I would try is create a custom validator class (maybe named CustomUploadValidator) that extends Upload_Validator (source can be found in the Upload.php file in the framework). So, something along those lines:
$myValidator = new CustomUploadValidator();
$uploadField->setValidator($myValidator);
In your custom validator class maybe create a method isImageLargeEnough() which you would call in the validate() method:
public function validate() {
if(!$this->isImageLargeEnough()) {
$this->errors[] = 'Image size is not large enough';
return false;
}
return parent::validate();
}
In your isImageLargeEnough() you can access the uploaded image through $this->tmpFile. So maybe do something like:
public function isImageLargeEnough()
{
$imageSize = getimagesize( $this->tmpFile["tmp_name"] );
if ($imageSize !== false)
{
if ( $imageSize[0] < 500 || $imageSize[1] < 500 )
{
return false;
}
}
return true;
}
Here the min width/height are hard coded to 500, but you can probably implement a setMinImageSizes method that stores those on a variable in your custom validator class. which could be called like $uploadField->getValidator()->setMinImageSize(543, 876);
None of this is actually tested, but hopefully it can give you some pointers on what to look for.

Laravel 4 Model Events don't work with PHPUnit

I build a model side validation in Laravel 4 with the creating Model Event :
class User extends Eloquent {
public function isValid()
{
return Validator::make($this->toArray(), array('name' => 'required'))->passes();
}
public static function boot()
{
parent::boot();
static::creating(function($user)
{
echo "Hello";
if (!$user->isValid()) return false;
});
}
}
It works well but I have issues with PHPUnit. The two following tests are exactly the same but juste the first one pass :
class UserTest extends TestCase {
public function testSaveUserWithoutName()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // pass
assertEquals($count, User::all()->count()); // pass
}
public function testSaveUserWithoutNameBis()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // fail
assertEquals($count, User::all()->count()); // fail, the user is created
}
}
If I try to create a user twice in the same test, it works, but it's like if the binding event is present only in the first test of my test class. The echo "Hello"; is printed only one time, during the first test execution.
I simplify the case for my question but you can see the problem : I can't test several validation rules in different unit tests. I try almost everything since hours but I'm near to jump out the windows now ! Any idea ?
The issue is well documented in Github. See comments above that explains it further.
I've modified one of the 'solutions' in Github to automatically reset all model events during the tests. Add the following to your TestCase.php file.
app/tests/TestCase.php
public function setUp()
{
parent::setUp();
$this->resetEvents();
}
private function resetEvents()
{
// Get all models in the Model directory
$pathToModels = '/app/models'; // <- Change this to your model directory
$files = File::files($pathToModels);
// Remove the directory name and the .php from the filename
$files = str_replace($pathToModels.'/', '', $files);
$files = str_replace('.php', '', $files);
// Remove "BaseModel" as we dont want to boot that moodel
if(($key = array_search('BaseModel', $files)) !== false) {
unset($files[$key]);
}
// Reset each model event listeners.
foreach ($files as $model) {
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Reregister them.
call_user_func(array($model, 'boot'));
}
}
I have my models in subdirectories so I edited #TheShiftExchange code a bit
//Get all models in the Model directory
$pathToModels = '/path/to/app/models';
$files = File::allFiles($pathToModels);
foreach ($files as $file) {
$fileName = $file->getFileName();
if (!ends_with($fileName, 'Search.php') && !starts_with($fileName, 'Base')) {
$model = str_replace('.php', '', $fileName);
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Re-register them.
call_user_func(array($model, 'boot'));
}
}

Resources