Essentially I have a View called 'promo' that's built on a Nodequeue. It is being restricted to 1 row, and order by Global:Random
The view itself is being used for a block on the site.
Additionally, the view same view is being for a node reference field 'field_promo'.
What I would like to do is to obtain the 2 most recent rows from the promo view, and use these as the default values for field_promo.
What this requires is that I:
load the view
remove the global:random sort
add a created sort
change pager to display 2 rows instead of 1
execute the view
...
Since writing all this and making sure I'm not an idiot, I've gathered together the solution which I may as well post below since it took me so long to find it! (will post solution after 8 hours, I'll give points if you crack the answer before I get to post :P)
Here is the solution I came up with:
$view = views_get_view('promo_feature');
$view->init_display();
$view->preview=TRUE;
$view->is_cacheable = FALSE;
$view->display_handler->set_option('items_per_page',2);
$view->set_item('default', 'sort', 'random', NULL);
$view->add_item('default', 'sort', 'node', 'created',array('order' => 'DESC'));
$view->pre_execute();
$output = $view->display_handler->preview();
$view->post_execute();
$return=array();
foreach($view->result as $row){
$return[]=array('nid' => $row->nid);
}
return($return);
$view->set_item($display,$type,$id,NULL) removes that item from the view
I suspect there's a more refined version of this code, however it took me so long to get there I'm afraid to touch it in case I destroy it :)
Related
I've been struggling with this for a while but can't find a clean way to do it, so I'm seeking for some help.
I have custom filters (ApiPlatform 2.5 and Symfony 5.1) on database API outputs, and I need to filter on the current workflow place, or status as you like, of each output.
The Status has the below structure, which is a symfony workflow's place :
Status = { "OPEN": 1 }
My issue is that the status is stored as an array in the DB, and I can't find a way to have the querybuilder finding a match.
I've tried to build locally an array to do an = , a LIKE or an IN :
$status['OPEN'] = 1;
$queryBuilder->andWhere(sprintf('%s.Status = :st', $rootAlias))
->leftJoin(sprintf('%s.Objs', $rootAlias), 'o')
->andWhere('o.Profile = :p')
->setParameters(array(
'st' => $status,
'p' => $profile
));
But no way :(
I implemented a workaround that works but I don't like it as I'm using workflows a lot and need a clean way to filter outputs.
My workaround is fairly simple, when the status is writen as an array in the DB, I also store it as a string in another field called StatusText, then filtering on StatusText is easy and straight.
Status can have different contents obviously : OPEN, CLOSING, CLOSED, ...
Help appreciated !!
Thanks
EDIT & Solution
As proposed by Youssef, use scienta/doctrine-json-functions and use JSON_EXTRACT :
composer require scienta/doctrine-json-functions
Important, that was part of my issue, use the Doctrine type json_array an not array to store the status or the state, however you call it, in the Database.
Integrate the alias provided inside the ApiPlatform custom filter :
$rootAlias = $queryBuilder->getRootAliases()[0];
$json_extract_string = "JSON_EXTRACT(".$rootAlias.".Status, '$.OPEN') = 1";
$queryBuilder->andwhere($json_extract_string )
->leftJoin(sprintf('%s.Objs', $rootAlias), 'o')
->andWhere('o.Profile = :p')
->setParameter('p', $profile);
You need to ask Doctrine if the JSON array contains the status, but you can't do that with the QueryBuilder method.
When you hit the ORM limitations you can use a Native Query with ResultSetMapping. It allows you to write a pure SQL query using specific features of your DBMS but still get entity objects.
Or you can use scienta/doctrine-json-functions and use JSON_EXTRACT
A user create a Post (Entity) and define a UrlKey field (input type url-path)
I want prevent a duplicate value (if another entity of this type already had a urlkey with same content)
Is there a way to accomplish that ?
* Edited *
Looking to the 2sxc code I didnt found a simple way to do that.
One thing that I have in mind is to create a ApiController/Endpoint that I can call and make the validation that I want, but for this I need to change the view from the Edit Content (for the user, not the admin one).
I found to the save in /dist/ng-edit/main.js that is minified, there I could change and call my controller/endpoint, but to change to show some feedback messages to user and after call the original endpoint is difficult with minified file.
Is a possibility to have the code that generate the main.js ? (Maybe is already there, and I couldnĀ“t found)
I am not aware of a built in (easy) way to accomplish this. But doing it in the View with a warning is probably a good fallback option. Assuming you have a View (ListContent) that shows a group of Posts, you could add something like this, though it is catching it AFTER the fact...
I just did something like this a few weeks ago because a client kept repeating Titles in the Blueimp Gallery app. So you can just drop this in to _AlbumList Bootstrap.cshtml at line 4, create a few duplicates (Titles which will generate duplicate Paths) and you should see an option to fix them... So, after the fact, if there are duplicates and the current user is Super/Admin/ContentManager, then let them know the situation and make it easy to fix:
#using DotNetNuke.Entities.Portals #* used by IsAdminOrHost() *#
#using DotNetNuke.Entities.Users #* used by IsAdminOrHost(), IsContentManager() *#
#{
if(IsContentManager(Dnn.User)){
var query = AsDynamic(Data["Default"])
.GroupBy(x => x.Path)
.Where(g => g.Count() > 1)
.Select(y => y.Key)
.ToList();
if(query != null) {
<pre>
You have duplicates (#query.Count()), please fix them:
#foreach(var dupe in AsDynamic(Data["Default"])
.Where(p => query.Any(x => x == p.Path)) ) {
<span #Edit.TagToolbar(dupe)>-- #dupe.EntityId, #dupe.Path</span>
}
</pre>
}
}
}
#functions {
// is the current user an Admin or Host?
public bool IsAdminOrHost(UserInfo userInfo) {
return userInfo.IsSuperUser || userInfo.IsInRole(PortalSettings.Current.AdministratorRoleName);
}
// is the current user a Content or Site Manager (or Admin+)?
public bool IsContentManager(UserInfo userInfo) {
return userInfo.IsInRole("Content Managers")
|| userInfo.IsInRole("Site Managers")
|| IsAdminOrHost(userInfo);
}
}
Results should look something like this:
I use event listener for change data dynamically based on user inputs. Each time I use PRE_SET_DATA and PRE_SUBMIT events for set data and fields choices. Here is the simple example of actions from PRE_SUBMIT:
// Pre set share locations by share day
if (array_key_exists('shares', $data)) {
foreach ($data['shares'] as $key => $share) {
if ($share['pickUpDay'] !== null) {
$shareType = $form->get('shares')->get($key);
$locations = $this->em->getRepository('AppBundle:Member\Location')->getLocationsByDay($client, $data['shares'][$key]['pickUpDay']);
$this->addLocationField($shareType, $locations);
}
}
}
Not matter what inside addLocationField function, it works right.
When I do $form->get('shares'), its my collection field, then I need to ->get(child) of this collection and set fields data and choices straight to this child. By when I add collection dynamically, Symfony shows error:
Child "n" does not exist.
And this problem happens only when I try to get data of new collection that was added dynamically. So I can't get to a collection field and change choices, so I receive error that my new value is not in a choice list.
Interesting that $data['shares'] have all data for new collection elements, but $form->get('shares') haven`t:
var_dump(count($event->getData()['shares'])) - return 1;
var_dump(count($form->get('shares'))) - return 0;
Is that mean that my PRE_SUBMIT works before Symfony collection functionality happen?
Someone know how to fix it?
I know your question is "old" and you probably found a solution but you were in the right direction when you said :
Is that mean that my PRE_SUBMIT works before Symfony collection functionality happen?
Your new collection is not submitted yet and it is not present in the model see this part of the doc
To make what you want to, you should use the SUBMIT event
NB : You can't add any field on POST_SUBMIT
After hours of trying and searching, I think its time to share my problem with you right now.
Problem Definition :
I have a Dictionary of KeyValuePairs(named filterPool) which includes an integer (PropertyID) and a string(McValue). What I am trying to do is filtering products depending on those KeyValuePairs and return them as a DataTable/List.
You may consider this as building dynamic "Where ... And .." clauses as SQL.
Here is the code that I am using :
foreach (KeyValuePair<int, string> filter in filterPool)
{
products = products.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value));
}
return products.ToDataTable();
The problem is the foreach loop above seems to work only once, for the latest KeyValuePair available in the Dictionary.
As far as I could find on Stackoverflow, the closest solution to my problem was : this one, also using a Dictionary of values for filtering
There must be a way to achieve the goal of filtering using Dictionary and LINQ; or there's a huge thing that I am missing/ignoring to see somehow.
Hope the problem given is clear enough for all,
Thanks
^^
This is a closure issue. You can solve it by making a temporary:
foreach (KeyValuePair<int, string> filterTmp in filterPool)
{
var filter = filterTmp; // Make a temporary
products = products.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value));
}
return products.ToDataTable();
For details on what's happening, see Eric Lippert's post Closing over the loop variable considered harmful.
Also note that this behavior has changed for C# 5. In C# 5/VS2012, this code would work as expected.
You're overwriting your products collection on every iteration of your foreach. I'm not sure what the data type on your collection is, but you'll want to do something like this in your foreach instead:
products.AddRange(products.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value)));
I'm not sure if that makes sense, but it seems like you're trying to create a collection full of products that match your filterPool.
I think that it's better solved with aggregate:
return filter
.Aggregate(products, (acc, filter) => acc.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value)));
.ToDataTable();
I'm trying to show updated results for a CCK Computed Field.
The computation is based on fields in another node, so are not being automatically updated.
So: I'm calling node_save($node) in hook_view, which does make the adjustment but the results don't show until I refresh the page.
Is there a way to refresh the page automatically, or should I be approaching this from a different angle?
Edit: In response to Henrik's questions, here's more detail:
The hook_view and its node_save are below, the rest of the code is in a Computed Field in the 'project' content type, summing up values from another node. Without the node_save, I have to edit and save the 'project' node to get the result. With it, I just need to refresh the page.
Adding drupal_goto(drupal_get_destination()) in the hook_view gives a 'page not found', rather than the vicious loop I was expecting. Is there another place I could put it?
function mymodule_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
switch ($op) {
case 'view':
if($node->type == 'project') {
project_view($node);
break;
}
}
}
function project_view($node) {
node_save($node);
return $node;
}
Edit 1: Given the newly posted code and additional explanations, I have three suggestions that might solve the problem without redirecting:
As project_view() does not take the node argument by reference, you might want to actually grab its (potentially updated) result in mymodule_nodeapi by writing
$node = project_view($node);
instead of just
project_view($node);
If that works, it should also work without the indirection via project_view() by just calling node_save($node) directly in mymodule_nodeapi. (node_save() takes the node argument by reference).
AFAIK, computed fields basically provide two working modes that you can switch via checkbox on the field configuration form:
Computing the field once on node_save(), storing the result in the database, updating only on new save operations.
Not storing the field at all, instead recomputing it every time the node is viewed.
Have you tried the 'always recompute' option already?
Edit 2: My original answer was flawed in two ways at once, as it used a completely wrong function to retrieve the current request URI and did not check for recursion (as lazysoundsystem pointed out very courteously ;)
So the following has been updated to an actually tested version of doing the redirection:
Is there a way to refresh the page
automatically
You could try:
if (!$_REQUEST['stop_redirect']) {
drupal_goto(request_uri(), array('stop_redirect' => true));
}
This will cause Drupal to send a redirect header to the client, causing a new request of the current page, making sure not to redirect again immediately.
If the value is only ever going to be computed, you could just add something to your node at load time.
function mymodule_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
switch ($op) {
case 'load':
if($node->type == 'project') {
$node->content['myfield'] = array('#value' => mymodule_calculate_value(), '#weight' => 4, '#theme' => 'my_theme');
}
break;
}
}
}