Say my User model's balance attribute from getBalanceAttribute() returns the sum of amount from user's Transaction model, how can this be orderable in the Datatable?
User.php
public function transactions()
{
return $this->hasMany(\App\Transaction::class);
}
public function getBalanceAttribute()
{
return $this->transactions()->sum('amount');
}
Transaction.php
public function user()
{
return $this->belongsTo(\App\User::class);
}
UserCrudController.php
...
public function setup()
{
...
$this->crud->addColumn(
[
'name' => "balance",
'label' => "Balance",
'type' => 'number',
// Here the column is clickable but is not actually sorted.
'orderable' => true,
],
...
}
Thank you in advance!
Unfortunately, Backpack cannot make a model_function column orderable, since model functions are called after the SQL has already gotten back.
Sorry.
Related
I am using REST API in my project and everything works great. I describe a model using a model
<?php
namespace api\modules\v1\models;
use Carbon\Carbon;
use Yii;
class Comment extends \common\models\Comment
{
public function fields()
{
return [
'id',
'user' => function(Comment $model) {
return User::findOne($model->user_id);
},
'text',
'image' => function(Comment $model) {
return Yii::$app->params['link'].$model->image;
},
'created_at' => function(Comment $model) {
Carbon::setLocale(Yii::$app->language);
return Carbon::createFromTimeStamp(strtotime($model->created_at))->diffForHumans();
},
'children' => function(Comment $model) {
$comments = Comment::find()
->where(['comment_id' => $model->id]);
if (!$comments->exists()) {
return false;
}
return $comments->all();
},
'like',
'news_id',
'comment_id'
];
}
}
The data is returned in the specified format and that's great. But I need to send data to the controller using websockets. For example, when a new comment arrives, send it to all users.
$post = Yii::$app->request->post();
$image = UploadedFile::getInstanceByName('image');
$model = new \api\modules\v1\models\Comment([
'news_id' => $post['feed_id'],
'comment_id' => $post['comment_id'] ?? null,
'user_id' => Yii::$app->user->identity->id,
]);
$model->text = $model->findLinks($post['text']);
if ($image && !$image->error) {
if (!file_exists(Yii::$app->params['comment.pathAbsolute'])) {
if (!FileHelper::createDirectory(Yii::$app->params['comment.pathAbsolute'], 0777)) {
throw new \Exception('Помилка створення папки');
}
}
$serverName = Yii::$app->security->generateRandomString(16).'.'.$image->extension;
if ($image->saveAs(Yii::$app->params['comment.pathAbsolute'].$serverName)) {
$model->image = $serverName;
} else {
throw new \Exception($image->error);
}
}
if (!$model->save()) {
throw new \Exception($model->error());
}
Helper::ws(false, 'updateComment', ['feed_id' => $post['feed_id'], 'comment' => $model]);
And when I pass the $model, the data is passed as it is stored in the database. Is it possible to call a method or something so that the data is passed as I described in the model api?
i'm trying to redirect from component if id from slug is wrong.
Running from layout
function onBeforePageStart(){ $this->Contentloader->getMeta(); }
In component i have:
public function getMeta(){
//id checking logic goes here
if ($id == null) return Redirect::to('/'); }
Inspecting the dd(Redirect::to('/')) object I see
But it's not redirecting.
Please advice
Thanks
try this
in your component :
public function getMeta()
{
if ($id == null) return false;
}
in your layout :
function onBeforePageStart()
{
$meta = $this->Contentloader->getMeta();
if(!$meta)
return Redirect::to('/');
}
I hope help you :)
Components should be able to handle redirects without having onBeforePageStart(). This is just a quick example. Here I am checking to see if a component field is null. If it is null then return to '/'.
You can do this in a component: Make sure to utilize the Redirect class use Redirect;
public function defineProperties()
{
return [
'useSomething' => [
'title' => 'Something',
'description' => 'Testing Testing',
'default' => '',
'type' => 'text',
]
];
}
public function onRun()
{
if ($this->property('useSomething') == null) {
return Redirect::to('/');
} else {
$this->page['something'] = $this->property('useSomething');
}
}
I have two relations
public function getAnnounceComments()
{
return $this->hasMany(AnnounceComment::className(), ['user_id' => 'id']);
}
public function getAnnounceRates()
{
return $this->hasMany(AnnounceRate::className(), ['user_id' => 'id']);
}
How can I merge User's two relations as one on same announce_id then I can call this like
$user->commentAndRate->comment or $user->commentAndRate->rate
I did like that:
Added this relation to AnnounceComment model:
public function getRate()
{
return $this->hasOne(AnnounceRate::className(), ['announce_id' => 'announce_id']);
}
And when I call User did like this:
$user = User::find()->with('announceComments.rate')->where('id=1')->one()
Then I can easily call :
foreach ($model->announceComments as $key => $comment) {
echo $comment->text . $comment->rate->count`
}
I wait for better answers :)
how validate entity variable, because in my point it's valid for empty select.
/**
* #Assert\NotBlank(message = "education level cannot be empty")
* #var EducationLevel[]|ArrayCollection
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\EducationLevel")
*/
private $educationLevel;
form type
->add('educationLevel', EntityType::class, [
'class' => 'AppBundle:EducationLevel',
'multiple' => true,
'choice_label' => function ($educationLevel) {
return $educationLevel->getName();
},
])
NotBlank won't work, because it checks if value is not null or not empty string or not false
NotBlank manual
What you have to do is to write custom Constraint and a validator:
Custom validator manual
You can create a validation method in the entity which can verify if $educationLevel is null or collection of EducationLevel instances.
/**
* #Assert\IsTrue(message="Education level has to be blank or...")
*/
public function isEducationLevelValid()
{
if ($this->educationLevel->isEmpty()) {
foreach ($this->educationLevel as $edulevel) {
if (!$edulevel instanceof EducationLevel) {
return false;
}
}
return true;
} else {
return false;
}
}
The method is used automatically during entity bound form submission and of course you can use it as a normal entity's method.
I have this model:
Banner:
columns:
filename: string(255)
url: string(255)
position:
type: enum
values: [top, right]
default: right
and this form:
class BannerForm extends BaseBannerForm
{
public function configure()
{
$this->widgetSchema['filename'] = new sfWidgetFormInputFileEditable(array(
'file_src' => $this->getObject()->getThumbURL(),
'is_image' => true,
'edit_mode' => $this->getObject()->exists()
));
$validated_file_class = $this->getObject()->position === 'right' ? 'bannerRightValidatedFile' : 'bannerTopValidatedFile';
$this->validatorSchema['filename'] = new sfValidatorFile(array(
'path' => sfConfig::get('sf_upload_dir'),
'mime_types' => 'web_images',
'validated_file_class' => $validated_file_class',
'required' => $this->getObject()->isNew()
));
}
}
I use different validate classes because inside it i incapsulate thumbnail operations, and the sizes of banners depends on it position field.
The problem is that $validated_file_class is always bannerRightValidatedFile class.
How i can achieve this thing ?
I can suggest 4 solutions which you can choose from:
Option 1:
You should add a update$fieldNameColumn method to the form class. In your case it should look like this:
// change validated file instance before calling save
protected function updateFilenameColumn($value)
{
if ($value instanceof sfValidatedFile)
{
$class = 'right' == $this->getValue('position') ? 'bannerRightValidatedFile' : 'bannerTopValidatedFile';
// this will not work as I thought at first time
// $this->getValidator('filename')->setOption('validated_file_class', $class);
$this->values['filename'] = new $class(
$value->getOriginalName(),
$value->getType(),
$value->getTempName(),
$value->getSize(),
$value->getPath()
);
return $this->processUploadedFile('filename');
}
return $value;
}
I think it's kind of hacky.
Option 2:
You should add a doctrine hook method to the model:
/**
* #param Doctrine_Event $event
*/
public function postSave($event)
{
$record = $event->getInvoker();
if (array_key_exists('filename', $record->getLastModified()))
{
// get the full path to the file
$file = sfConfig::get('sf_upload_dir') . '/' . $record->getFilename();
if (file_exists($file))
{
// resize the file e.g. with sfImageTransformPlugin
$img = new sfImage($file);
$img
->resize(100, 100)
->save();
}
}
}
This will work when creating records whitout a form e.g. when using fixtures.
Option 3:
Use the admin.save_object event.
public static function listenToAdminSaveObject(sfEvent $event)
{
$record = $event['object'];
if ($event['object'] instanceof Banner)
{
// use the same code as in the `postSave` example
}
}
Option 4:
Use the sfImageTransformExtraPlugin
It's kind of hard to setup and configure (and it's code is a mess :), but it makes possible to modify the size of the image whithout regenerating all the already resized ones.
You could add a sfCallbackValidator as a post-validator, and set the property accordingly.
Pseudo code (I don't have the exact function signatures at hand).
public function configure() {
// ...
$this->mergePostValidator(new sfCallbackValidator(array('callback' => array($this, 'validateFile'))));
}
public function validateFile($values) {
$realValidator = new sfValidatorFile(...);
return $realValidator->clean($values['field']);
}
If you can modify the call to the form class, you can do that:
$form = new BannerForm(array(), array('validated_file_class' => 'bannerRightValidatedFile');
$form2 = new BannerForm(array(), array('validated_file_class' => 'bannerTopValidatedFile');
And then in your form:
class BannerForm extends BaseBannerForm
{
public function configure()
{
$this->widgetSchema['filename'] = new sfWidgetFormInputFileEditable(array(
'file_src' => $this->getObject()->getThumbURL(),
'is_image' => true,
'edit_mode' => $this->getObject()->exists()
));
$this->validatorSchema['filename'] = new sfValidatorFile(array(
'path' => sfConfig::get('sf_upload_dir'),
'mime_types' => 'web_images',
'validated_file_class' => $this->options['validated_file_class'],
'required' => $this->getObject()->isNew()
));
}
}
Edit:
Since you are playing inside the admin gen, I think the best way is to use a postValidator like #Grad van Horck says.
Your validate class depend on an extra field. With a postvalidator, you can access any field inside the form. Then, you just need to create a little switch to handle the case for each position / validated class.
public function configure()
{
// ...
$this->mergePostValidator(new sfValidatorCallback(array('callback' => array($this, 'validateFile'))));
}
public function validateFile($validator, $values, $arguments)
{
$default = array(
'path' => sfConfig::get('sf_upload_dir'),
'mime_types' => 'web_images',
'required' => $this->getObject()->isNew()
);
switch ($values['position'] ) {
case 'right':
$validator = new sfValidatorFile($default + array(
'validated_file_class' => 'bannerRightValidatedFile',
));
break;
case 'top':
$validator = new sfValidatorFile($default + array(
'validated_file_class' => 'bannerTopValidatedFile',
));
default:
# code...
break;
}
$values['filename'] = $validator->clean($values['filename']);
return $values;
}