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.
Related
I want to programmatically alter a page title in Drupal 8 so that it will be hard-coded in the theme file.
I'm attempting to use a hook function to preprocess_page_title, but it seems to not understand what page to change the title on.
Here's what I have so far:
function test_preprocess_page_title(&$variables) {
if (arg(0) == 'node/12') {
$variables['title'] = 'New Title';
}
}
I figured the only way to make this change on one specific page is to set the node argument. Has any one figured out a way to override page title on Drupal?
In your template.theme file add the preprocessor and then override page-title.html.twig in your template folder by printing the variable, like below:
function theme_preprocess_page_title(&$variables) {
$node = \Drupal::request()->attributes->get('node');
$nid = $node->id();
if($nid == '14') {
$variables['subtitle'] = 'Subheading';
}
}
then {{ subtitle }}
Here's the method to preprocess your page :
function yourthemename_preprocess_page(&$variables) {
$node = \Drupal::routeMatch()->getParameter('node');
if ($node) {
$variables['title'] = $node->getTitle();
}
}
and in your template page.html.twig
{{title}}
There are a couple of solutions to change the page title
On template
/**
* Implements hook_preprocess_HOOK().
*/
function MYMODULE_preprocess_page_title(&$variables) {
if ($YOUR_LOGIC == TRUE) {
$variables['title'] = 'New Title';
}
}
On the node view page
/**
* Implements hook_ENTITY_TYPE_view_alter().
*/
function mymodule_user_view_alter(array &$build, Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display) {
if ($YOUR_LOGIC == TRUE) {
$build['#title'] = $entity->get('field_display_name')->getString();
}
}
for a sample if you want to change user title
function mymodule_user_view_alter(array &$build, Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display) {
if ($entity->getEntityTypeId() == 'user') {
$build['#title'] = $entity->get('field_first_name')->getString();
}
}
On Controller or hook_form_alter
if ($YOUR_LOGIC == TRUE) {
$request = \Drupal::request();
if ($route = $request->attributes->get(\Symfony\Cmf\Component\Routing\RouteObjectInterface::ROUTE_OBJECT)) {
$route->setDefault('_title', 'New Title');
}
}
The Page Title is a block in Drupal 8. If you can find the plugin_id of the page title block (which is likely to be page_title_block), then you can override the title directly, with no need to change an existing twig template, using a block preprocessor. Your code may be similar to the following:
function vhs_preprocess_block(&$variables) {
// This example restricts based on the actual URL; you can replace this with any other logic you wish.
$request = \Drupal::request();
$uri = $request->getRequestUri();
if (
isset($variables['elements']['#base_plugin_id']) &&
$variables['elements']['#base_plugin_id'] == 'page_title_block' &&
isset($variables['content']['#title']['#markup']) &&
strpos($uri, '/url-to-match') === 0 // replace with logic that finds the correct page to override
) {
$variables['content']['#title']['#markup'] = 'My Custom Title';
}
}
The example above uses the Drupal request object to grab and compare the actual URL. The initial question asked to match based on the node path; you could get that with something like:
$current_path = \Drupal::service('path.current')->getPath();
Then, in place of the strpos condition above, you could use:
$current_path == 'node/12'
i have changed the page_title block for user/uid to a different custom account field name like this :
function hook_preprocess_block(&$variables) {
$path = \Drupal::request()->getpathInfo();
$arg = explode('/', $path);
if (isset($arg[2]) && $arg[2] == 'user' && isset($arg[3])) {
if (isset($variables['elements']['content']['#type']) && $variables['elements']['content']['#type'] == 'page_title') {
$account = \Drupal\user\Entity\User::load($arg[3]);
if(isset($account) && isset($account->field_mycustomfield->value)){
$variables['content']['#title']['#markup']=$account->field_mycustomfield->value;
}
}
}
}
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.
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.
I have set a field called Colour in Page.php and for any child I would like to grab the parent colour or loop through till it finds a parent that does have the colour field set.
I have a function below which seems to work in 2.4 but I cannot get to work in SS3 which I call inside a loop in templates as $Inherited(Colour).
Your help is appreciated
public function Inherited($objName) {
$page = $this->owner->Data();
do {
if ($obj = $page->obj($objName)) {
if ($obj instanceof ComponentSet) {
if ($obj->Count()) {
return $obj;
}
} elseif ($obj instanceof DataObject) {
if ($obj->exists()) {
return $obj;
}
} elseif ($obj->exists()) {
return $obj;
}
}
} while ($page->ParentID != 0 && $page = $page->Parent());
}
Assuming your Colour field is a database field and not a relationship to another data object, add the following method to your Page class.
public function getColour() {
// Try returning banners for this page
$colour = $this->getField('Colour');
if ( $colour ) {
return $colour;
}
// No colour for this page? Loop through the parents.
$parent = $this->Parent();
if ( $parent->ID ) {
return $parent->getColour();
}
// Still need a fallback position (handled by template)
return null;
}
If colour is a related data object you could do much the same thing but use the getComponent or getComponents method in place of getField in the code above. This should work on both Silverstripe version 2.4.x and 3.0.x.
This kind of operation, although useful, should probably be done sparingly or be heavily cached as it's recursive could conceivably happen on the majority of page loads, and change very infrequently.
i suppose you've had this function defined inside some DataObjectDecorator, as you're using $this->owner to refer to the current page.
there is no more DataObjectDecorator in SilverStripe 3 (see http://www.robertclarkson.net/2012/06/dataextension-class-replacing-dataobjectdecorator-silverstripe-3-0/) so there are two possible solutions:
a) replace DataObjectDecorator by DataExtension
b) simply move the Inherited function to your Page class, and replace $this->owner by $this
I'm quite new to flex/actionscript and I was wondering if there is an equivalent for php's (and other languages) FILE and LINE identifiers?
Basicly I want to do some custom error logging and would like to something like:
var mymessage:String = 'Oops, a hiccup occured at ' + __FILE__ + ', line: ' + __LINE__;
Where file and line would ofcourse be substituted for their values at compile time.
Is this possible?
It's not directly possible, but there's a fairly usable workaround for personal testing
var stackTrace:String = new Error().getStackTrace();
if (stackTrace) {
var mymessage:String = "Oops, a hiccup occurred " + stackTrace.split("\n")[1];
}
Adjust your abuse of getStackTrace to taste.
To add to Cory's answer to the above. First add:
-define=CONFIG::debugging,true
to your library's compiler settings (next to the "-locale en_US" in "Additional Compiler Arguments"). Then use this quickie library:
package ddd
{
public class Stack
{
protected static function str(val:*):String
{
if( val == null ) return "<null>";
if( val == undefined ) return "<undefined>";
return val.toString();
}
protected static var removeAt :RegExp = /^\s*at\s*/i;
protected static var matchFile:RegExp = /[(][)][\[][^:]*?:[0-9]+[\]]\s*$/i;
protected static var trimFile :RegExp = /[()\[\]\s]*/ig;
/* Must maintain number of stack levels, so that _stack can assume the 4th line of getStackTrace */
private static function _stack( msg:String="", ...params ):String
{
var s :String = new Error().getStackTrace();
var func:String = "??";
var file:String = "??";
var args:String = null;
if(s)
{
func = s.split("\n")[4];
func = func.replace( removeAt, "" );
var farr:Array = func.match( matchFile );
if( farr != null && farr.length > 0 ) file = farr[0].replace( trimFile, "" );
func = func.replace( matchFile, "" );
}
for each( var param:* in params )
{
args = ( args == null ? "" : args.concat(",") );
args = args.concat( str(param) );
}
return func + "(" + (args==null?"":args) + ")" + ( (msg!=null && msg!="") ? ":"+msg : "" ) + " at " + file;
}
/* Must maintain number of stack levels, so that _stack can assume the 4th line of getStackTrace */
public static function stack( msg:String="", ...params ):String
{
params.unshift( msg );
return _stack.apply( null, params );
}
/* Must maintain number of stack levels, so that _stack can assume the 4th line of getStackTrace */
public static function pstack( msg:String="", ...params ):void
{
CONFIG::debugging {
params.unshift(msg);
trace( _stack.apply( null, params ) );
}
}
}
}
And then you can just call:
Stack.pstack();
inside any function to print the stack location at that point, which looks like this:
package::classname/function() at /wherever/src/package/classname.mxml:999
Just remember to turn debugging to false before compiling for production, and all that will be left is an empty pstack call that does nothing - the guts will be conditional-compiled out.
IMHO the line or file doesn't add to much information in Flex. I usually output class and method name and as my methods tend to be short, it usually is clear where something occurred.
If you find yourself with methods that are hundreds of lines long, you should rethink your coding style.