Symfony2 - Give a default filter in a list of elements of Sonata Admin - symfony

I have a list of elements of type Vehicle and I show these elements with Sonata Admin. I allow to filter these elements by the "status" field, but I want that, when the list is showed, only the active vehicles are showed, and if somebody wants to see the inactive vehicles, uses the filter and select the inactive status. I would like to know if somebody Knows the way to apply filters by default for a list of elements using Sonata Admin.
Here is my code:
public function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name')
->add('status')
;
}
protected function configureDatagridFilters(DatagridMapper $mapper)
{
$mapper
->add('name')
->add('status')
;
}
Is there any option that can be added to the status field in configureDatagridFilters() to achieve this goal? Other options?
Thanks in advance.

You have to override $datagridValues property as following (for status > 0 if it's an integer) :
/**
* Default Datagrid values
*
* #var array
*/
protected $datagridValues = array (
'status' => array ('type' => 2, 'value' => 0), // type 2 : >
'_page' => 1, // Display the first page (default = 1)
'_sort_order' => 'DESC', // Descendant ordering (default = 'ASC')
'_sort_by' => 'id' // name of the ordered field (default = the model id field, if any)
// the '_sort_by' key can be of the form 'mySubModel.mySubSubModel.myField'.
);
source: Configure the default page and ordering in the list view

You can also use this method
public function getFilterParameters()
{
$this->datagridValues = array_merge(
array(
'status' => array (
'type' => 1,
'value' => 0
),
// exemple with date range
'updatedAt' => array(
'type' => 1,
'value' => array(
'start' => array(
'day' => date('j'),
'month' => date('m'),
'year' => date('Y')
),
'end' => array(
'day' => date('j'),
'month' => date('m'),
'year' => date('Y')
)
),
)
),
$this->datagridValues
);
return parent::getFilterParameters();
}

Using both above suggested approaches will break the filters "reset" behaviour since we are always forcing the filter to filter by a default value. To me, i think the best approach is to use the getFilterParameters function (since we can add logic in there instead of statically add the value) and check if the user clicked the "Reset button"
/**
* {#inheritdoc}
*/
public function getFilterParameters()
{
// build the values array
if ($this->hasRequest()) {
$reset = $this->request->query->get('filters') === 'reset';
if (!$reset) {
$this->datagridValues = array_merge(array(
'status' => array (
'type' => 1,
'value' => 0
),
),
$this->datagridValues
);
}
}
return parent::getFilterParameters();
}

Since sonata-admin 4.0, the function getFilterParameters() is tagged as final and the $datagridValues doesn't exist anymore.
So you need to override the configureDefaultFilterValues() function
protected function configureDefaultFilterValues(array &$filterValues): void
{
$filterValues['foo'] = [
'type' => ContainsOperatorType::TYPE_CONTAINS,
'value' => 'bar',
];
}
More details: https://symfony.com/bundles/SonataAdminBundle/current/reference/action_list.html#default-filters

Another approach is to use createQuery and getPersistentParameters to enforce invisible filter. This approach is best to have fully customizable filters. See my articles here:
http://www.theodo.fr/blog/2016/09/sonata-for-symfony-hide-your-filters/

Related

How to retrieve subject in Sonata configureListFields?

I use Sonata Admin Bundle in my Symfony project and created an ArticleAdmin class for my Article entity.
In the list page, I added some custom actions to quickly publish, unpublish, delete & preview each article.
What I want to do is to hide publish button when an article is already published & vice versa.
To do this, I need to have access to each object in method configureListFields(). I would do something like this:
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->add('title');
// ...
/** #var Article article */
$article = $this->getSubject();
// Actions for all items.
$actions = array(
'delete' => array(),
'preview' => array(
'template' => 'AppBundle:ArticleAdmin:list__action_preview.html.twig',
),
);
// Manage actions depending on article's status.
if ($article->isPublished()) {
$actions['draft']['template'] = 'AppBundle:ArticleAdmin:list__action_draft.html.twig';
} else {
$actions['publish']['template'] = 'AppBundle:ArticleAdmin:list__action_preview.html.twig';
}
$listMapper->add('_actions', null, array('actions' => $actions));
}
But $this->getSubjet() always returns NULL. I also tried $listMapper->getAdmin()->getSubject() and many other getters but always the same result.
What am I doing wrong ?
Thanks for reading & have a good day :)
You can do the check directly in the _action template, as you can access the current subject.
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('title')
->add('_action', 'actions', array(
'actions' => array(
'delete' => array(),
'preview' => array(
'template' => 'AppBundle:ArticleAdmin:list__action_preview.html.twig',
),
'draft' => array(
'template' => 'AppBundle:ArticleAdmin:list__action_draft.html.twig',
),
'publish' => array(
'template' => 'AppBundle:ArticleAdmin:list__action_publish.html.twig',
),
))
;
}
And for example in AppBundle:ArticleAdmin:list__action_draft.html.twig, you can check your condition :
{% if object.isPublished %}
your html code
{% endif %}

Symfony2, Sonata, FormMapper, add hidden field to be handled in PrePersist/PreUpdate

I actually did some tricks so i could be able to persist a user if its ID is passed by an url parameter. (Custom action from user list).
/admin/se/api/bundle/create?user=7
I actually could not find how to send the user entity returned by a findByOne(array('id' => $user_id)) so i guess i'll need to pass the $user_id through a hidden field and handle its value in a PrePersist
Otherwise passing the id that way
->add('user', 'hidden', array('data' => $user_id))
will return an error :
This value is not valid.
Symfony\Component\Validator\ConstraintViolation
Object(Symfony\Component\Form\Form).children[user] = 7
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Compound forms expect an array or NULL on submission.
This is my first attempt that is not working :
$container = $this->getConfigurationPool()->getContainer();
$request = $container->get('request');
$user_id = $request->get('user');
if(!empty($user_id)){
$em = $this->getModelManager()->getEntityManager($this->getClass());
$user = $em->getRepository('ApiBundle:User')->findOneBy(array('id' => $user_id));
if($user){
$formMapper
->with('User', array('description' => '<strong>User : </strong>'.$user->getDisplayName()))
->add('user', 'hidden', array('data' => $user_id))
// this of course doesn't work as explained above. How can i have my own hidden input not related to any property
->end();
}
So how would i do that? Any better solution is welcomed.
Well this is the best trick i found. I wish 'sonata_type_model_hidden' has more options. I guess i could do my own custom field to be able to do that. But i'm not sure how and anyway this solution is fast to implement.
$formMapper
->with('Guide', array('description' => '<strong>Guide : </strong>'.$guide->getDisplayName()))
->add('guide', 'sonata_type_model_autocomplete', array(
'property' => array('firstname', 'lastname', 'username', 'email'),
'data_class' => null, // IMPORTANT
'data' => $guide,
'attr' => array('class' => 'sonata-autocomplete-hidden'), // custom class
'label_attr' => array('class' => 'sonata-autocomplete-hidden'), // custom class
)
)
->end();
To hide the field :
.sonata-autocomplete-hidden{
display:none;
}
If you have any better solutions, you're welcome.

Why is the invoice page different for another user?

When I go to admin/store/orders/50457/invoice as the administrator, I see the following:
Notice how it has a payment method of "Free order" and the total at the bottom is $0.00.
When I go to the same page as a non-administrator, I see the following:
Notice how Payment Method is empty and the total for the order is $8.21.
The correct one is what the administrator sees, so what is going on that makes the two behave differently?
EDIT: It turns out in a module I created (a long time ago) called tf_store_credit.module, I have the following:
function tf_store_credit_line_item(){
global $user;
$total_credit= uc_store_credit_f_get_user_credit($user->uid);
if ($total_credit>0){
$items[] = array
(
'id' => 'store-credit', // You will use this ID in the javascript
'title' => t('Store Credit'), // This is the text that will be displayed on the line item in the subtotal
'callback' => 'tf_store_credit_line_item_callback', // This is the callback function
'stored' => TRUE,
'default' => FALSE,
'calculated' => TRUE,
'display_only' => FALSE,
'weight' => 15,
);
}
return $items;
}
The problem is that it is getting the currently logged in user's store credit instead of the user who placed the order. So how would I go about getting the correct user?
In tf_store_credit.module, I modified it like this:
function tf_store_credit_line_item(){
global $user;
$total_credit= uc_store_credit_f_get_user_credit($user->uid);
if ($total_credit>0 || in_array("view_orders", $user->roles)){
$items[] = array
(
'id' => 'store-credit', // You will use this ID in the javascript
'title' => t('Store Credit'), // This is the text that will be displayed on the line item in the subtotal
'callback' => 'tf_store_credit_line_item_callback', // This is the callback function
'stored' => TRUE,
'default' => FALSE,
'calculated' => TRUE,
'display_only' => FALSE,
'weight' => 15,
);
}
return $items;
}
In uc_free_order.module, I did a similar thing so that "Payment method" showed up:
function uc_free_order_payment_method() {
global $user;
if ($user->name=='blah' || $user->name=='blah2' || in_array("view_orders", $user->roles)){
$methods[] = array(
'id' => 'free_order',
'name' => t('Free order'),
'title' => t('Free order - payment not necessary.'),
'desc' => t('Allow customers with $0 order totals to checkout without paying.'),
'callback' => 'uc_payment_method_free_order',
'checkout' => TRUE,
'no_gateway' => TRUE,
'weight' => 10,
);
return $methods;
}
}

Change formatted address order no longer working

Something appears to have changed in the new version of WooCommerce, this snippet to change the order of the items in a formatted address used to work fine....
add_filter( 'woocommerce_order_formatted_billing_address' , 'woo_custom_order_formatted_billing_address' );
/**
* woo_custom_order_formatted_billing_address
*
* #access public
* #since 1.0
* #return void
*/
function woo_custom_order_formatted_billing_address() {
$address = array(
'first_name' => $this->billing_first_name,
'last_name' => $this->billing_last_name,
'company' => $this->billing_company,
'address_1' => $this->billing_address_1,
'address_2' => $this->billing_address_2,
'city' => $this->billing_city,
'state' => $this->billing_state,
'postcode' => $this->billing_postcode,
'country' => $this->billing_country
);
return $address;
}
But now it returns the following error...
Fatal error: Using $this when not in object context in
Can anyone point me in the right direction of either what is going wrong or an alternative way of achieving it?
I'm seeing this example on the internet, but I don't know if it ever worked. First of because in 'woocommerce_order_formatted_billing_address' filter, that is applied in class-wc-order.php file, two arguments are provided, $address ARRAY, and a reference to current WC_Order OBJECT. But your definition of the filter function does not provide any arguments. Second, error that you receive describes the problem very accurately, pseudo-variable $this is available from within an object context, but your global function is not part of any object.
Enough with technicalities, the definition should look like this:
add_filter( 'woocommerce_order_formatted_billing_address' , 'woo_custom_order_formatted_billing_address', 10, 2 );
function woo_custom_order_formatted_billing_address( $address, $wc_order ) {
// make the changes to $address array here
// use for example, $wc_order->billing_first_name, instead of $this->billing_first_name
return $address;
}
#Eolis' answer doens't work for me, because Woocommerce applies WC()->countries->get_formatted_address( $address ) and this functions erases my new fields added to array on add_filter, so my solution is add field to previous field (first_name):
add_filter('woocommerce_order_formatted_billing_address', 'my_order_formatted_billing_address', 10, 2);
function my_order_formatted_billing_address($address, $wc_order) {
$billing_last_name_2 = get_post_meta( $wc_order->id, '_billing_last_name_2', true );
$address = array(
'postcode' => $wc_order->billing_postcode,
'last_name' => $wc_order->billing_last_name,
//\n force break line
'first_name' => $wc_order->billing_first_name . "\n" . $billing_last_name_2,
'address_1' => $wc_order->billing_address_1,
'address_2' => $wc_order->billing_address_2,
'city' => $wc_order->billing_city,
'state' => $wc_order->billing_state,
'country' => $wc_order->billing_country
);
return $address;
}

Drupal: Parent-child draggable table

So I've been going at this one for a while now. I'm trying to create a draggable table that has a parent-child relationship, but where the children cannot be moved out of the parent group, and all of the parents are sortable among each other. I've modeled my form and theme off of the admin menu code, and I have it duplicating that functionality. The problem is that I can move the children to another parent, or let it become a parent. As an illustration:
Category 1
|
|--Item 1
|--Item 2
Category 2
|
|--Item 3
|--Item 4
|--Item 5
I would like to be able to sort Item 1 and Item 2 with each other, and Item 3, Item 4, and Item 5 with each other, but not move them between Category 1 and Category 2. I also need to be able to sort Category 1 and Category 2 with one another, taking the children with them. I've went through so many combinations of $action, $group, $subgroup settings mixed with $class settings for the categories and items that I've lost track. Nothing I have tried so far has produced the desired result. Here's the relevant bits of my code as it is currently:
In my form:
$form['#tree'] = true;
foreach($categories as $cat) {
if(!isset($form['categories'][$cat->cid])){
$form['categories'][$cat->cid] = array(
'weight' => array(
'#type' => 'weight',
'#delta' => 25,
'#attributes' => array('class' => array('item-weight', 'item-weight-' . $cat->cid)),
),
'cid' => array(
'#type' => 'hidden',
'#value' => $cat->cid,
'#attributes' => array('class' => array('cid')),
),
);
foreach($cats[$cat->cid] as $item) {
$form['categories'][$cat->cid]['items'][$item->id] = array(
'weight' => array(
'#type' => 'weight',
'#delta' => 25,
'#default_value'=> $item->weight,
'#attributes' => array('class' => array('item-weight', 'item-weight-' . $cat->cid)),
),
'cid' => array(
'#type' => 'hidden',
'#value' => $cat->cid,
'#attributes' => array('class' => array('cid')),
),
);
}
}
}
In my theme:
$children = element_children($form['categories']);
$rows = array();
if(count($children) > 0) {
foreach($children as $cid) {
$row = array(
drupal_render($form['categories'][$cid]['weight']) .
drupal_render($form['categories'][$cid]['cid']),
);
$rows[] = array(
'data' => $row,
'class' => array('draggable', 'tabledrag-root'),
);
foreach(element_children($form['categories'][$cid]['items']) as $id) {
$row = array(
theme('indentation', array('size' => 1)) . drupal_render($form['categories'][$cid]['items'][$id]['name']),
drupal_render($form['categories'][$cid]['items'][$id]['weight']) .
drupal_render($form['categories'][$cid]['items'][$id]['cid']),
);
$rows[] = array(
'data' => $row,
'class' => array('draggable', 'tabledrag-leaf'),
);
}
drupal_add_tabledrag('cat-table', 'order', 'sibling', 'item-weight', 'item-weight-' . $cid);
}
}
drupal_add_tabledrag('cat-table', 'match', 'parent', 'cid', 'cid', 'cid', true, 1);
$output = theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => 'cat-table')));
$output .= drupal_render_children($form);
return $output;
I've read over the documentation for drupal_add_tabledrag(), looked at the code, looked at example code, and searched around drupal.org and Google, but haven't come up with anything.
My only solution so far is to copy and modify the tabledrag.js file to just eliminate those capabilities, but while stopping the indent problem with the items (meaning, not letting them be on the same as the categories), keeping them in the same category has been Not Fun.
I suppose the most important question is, using standard Drupal is this possible?
I know you've already done a lot of coding so you might not want to give it up at this point, but DraggableViews is great to accomplish this. You can set up a normal view and add this draggableviews filter, it adds a weight and optionally a parent reference. The view itself uses the same drag-n-drop system as the rest of Drupal's backend tables.
Alternatively you can use a term reference and tie taxonomy terms to nodes, and just use that drag-n-drop.
If I'm missing something in your needs, my apologies, just thought I'd offer this simpler solution as it has definitely served me well in the past. Best of luck either way.
Just finished adding this functionality to my module
https://github.com/player259/ajax_table
There is no help, demo is outdated, but I'm working on it from time to time
Sections support was achieved by overriding tabledrag.js functions
Use this snippet to insert table
$form['map'] = array(
'#type' => 'ajax_table',
'#header' => array(t('Element'), t('Settings'), t('Weight')),
'rows' => array(),
'#draggable' => array(
// drupal_add_tabledrag will be called in theme layer
// NULL first arg to apply to this table
array(NULL, 'match', 'parent', 'perfect-form-parent', 'perfect-form-parent', 'perfect-form-index'),
array(NULL, 'depth', 'group', 'perfect-form-depth', NULL, NULL, FALSE),
array(NULL, 'order', 'sibling', 'perfect-form-weight'),
),
'#draggable_groups' => array(),
);
foreach ($map as $i => $element) {
// ... some logic
$form['map']['rows'][$i] = array(
'data' => array(
'element' => array(),
'settings' => array(),
'tabledrag' => array(
'index' => array(
'#type' => 'hidden',
'#value' => $element['data']['tabledrag']['index'],
'#attributes' => array('class' => array('perfect-form-index')),
),
'parent' => array(
'#type' => 'hidden',
'#default_value' => $element['data']['tabledrag']['parent'],
'#attributes' => array('class' => array('perfect-form-parent')),
),
'depth' => array(
'#type' => 'hidden',
'#default_value' => $element['data']['tabledrag']['depth'],
'#attributes' => array('class' => array('perfect-form-depth')),
),
'weight' => array(
'#type' => 'weight',
'#delta' => $max_weight,
'#default_value' => $weight,
'#attributes' => array('class' => array('perfect-form-weight')),
),
),
),
'#attributes' => array('class' => array($row_class_current, $row_class_child)),
);
// This means that row with $row_class_child class could have as parent
// only row with $row_class_parent class
// NULL means root - there are no parents
$form['map']['#draggable_groups'][$row_class_child] =
$depth ? $row_class_parent : NULL;
}
I had a similar problem at work so posting here my solution since none i found worked correctly in all situation. It is done 100% in javascript, on the php side you just have to set tabledrag in match with parent on pid and sort with siblings on weight.
The current code work on the example module (tabledrag parent/child) to adapt it to your need, change the .example-item-pid by your class for the PID input field. You just need to add it to the example code to have it working and see if it corresponds to your need.
First function invalidate any attempt to drop elements that don't have the same parent (PID) than the target element.
Second Function bypass the dragRow function to drop the element in the correct place (= the last children of the target row) and at the right depth ( = same depth than the target row).
/**
* Invalidate swap check if the row target is not of the same parent
* So we can only sort elements under the same parent and not move them to another parent
*
* #override Drupal.tableDrag.row.isValidSwap
*/
// Keep the original implementation - we still need it.
Drupal.tableDrag.prototype.row.prototype._isValidSwap = Drupal.tableDrag.prototype.row.prototype.isValidSwap;
Drupal.tableDrag.prototype.row.prototype.isValidSwap = function(row) {
if (this.indentEnabled) {
if (row && $('.example-item-pid', this.element).val() !== $('.example-item-pid', row).val()) {
return false;
}
}
// Return the original result.
return this._isValidSwap(row);
}
/**
* Position the dragged element under the last children of the element target for swapping when moving down our dragged element.
* Removed the indentation, since we can not change parent.
* #override Drupal.tableDrag.row.dragRow
*/
Drupal.tableDrag.prototype.dragRow = function (event, self) {
if (self.dragObject) {
self.currentMouseCoords = self.mouseCoords(event);
var y = self.currentMouseCoords.y - self.dragObject.initMouseOffset.y;
var x = self.currentMouseCoords.x - self.dragObject.initMouseOffset.x;
// Check for row swapping and vertical scrolling.
if (y != self.oldY) {
self.rowObject.direction = y > self.oldY ? 'down' : 'up';
self.oldY = y; // Update the old value.
// Check if the window should be scrolled (and how fast).
var scrollAmount = self.checkScroll(self.currentMouseCoords.y);
// Stop any current scrolling.
clearInterval(self.scrollInterval);
// Continue scrolling if the mouse has moved in the scroll direction.
if (scrollAmount > 0 && self.rowObject.direction == 'down' || scrollAmount < 0 && self.rowObject.direction == 'up') {
self.setScroll(scrollAmount);
}
// If we have a valid target, perform the swap and restripe the table.
var currentRow = self.findDropTargetRow(x, y);
if (currentRow) {
if (self.rowObject.direction == 'down') {
/**
* When going down we want to position the element after the last children and not right under the currentRow
*/
// create a new row prototype with currentRow
var rowObject = new self.row(currentRow, 'mouse', self.indentEnabled, self.maxDepth, false);
// extract all children
var childrenRows = rowObject.findChildren();
// if we have children
if (childrenRows.length > 0) {
// we change the row to swap with the last children
currentRow = childrenRows[childrenRows.length - 1];
}
self.rowObject.swap('after', currentRow, self);
}
else {
self.rowObject.swap('before', currentRow, self);
}
self.restripeTable();
}
}
/**
* We have disabled the indentation changes since it is not possible to change parent.
*/
return false;
}
};

Resources