EasyAdmin 4 - generate URL with filter - symfony

I need to redirect my user to CRUD index where filter "STATUS = ACTIVE" is applied.
I've this:
$url = $this->adminUrlGenerator
->setController(Customer::class)
->generateUrl();
return $this->redirect($url);
But I can't find a way to add a filter to it. I've tried searching for something like:
->setFilter('Status', 'ACTIVE')
but without any luck. There is nothing in the docs. How to do it?

EasyAdmin handle filters in your url by adding multiple options to handle each filters case.
value to be compared with
value2 (Example: between value and value2)
comparison for "equal", "less than", "greater than" etc...
Filtering by Status ACTIVE would modify your url with
&filters[Status][comparison]=%3D&filters[Status][value]=ACTIVE
Note that here %3D is = encoded for the url, but using = would work as well.
So when using EA AdminUrlGenerator, you can use ->set to modify options.
You would get:
$url = $this->adminUrlGenerator
->setController(Customer::class)
->set('filters[Status][value]', 'ACTIVE')
->set('filters[Status][comparison]', '=')
->generateUrl();
I kept the case on Status, but if your property is in lowercase, do it here as well.

Related

Creating a URL rewrite rule which creates a URL finishing with a unique set of characters from an id parameter

I'm developing a WP plugin and have a WordPress URL with an id search parameter:
(e.g.: http://localhost/testsite1/coder/?id=66),
...which rewrites to...
http://localhost/testsite1/coder/66.
The id is obtained via a dB query of a record.
How can I convert the id to a unique set of characters...
(e.g.: 66 -> h6gt!2)
...and have a rewrite rule which will create a URL such that...
http://localhost/testsite1/coder/h6gt!2
...will actually mirror...
http://localhost/testsite1/coder/?id=66?
I think you would have to have two things in place:
:: First, a hook on post save that saves a meta property for the post, e.g. code and generate a hash or seeded unique code where the id is the seed for the id. You could use e.g.
hash('crc32', 66, false)
Mind you, a hash function is never truly unique though. I can come up with a simple random seeded code generator, but again, not sure if this could generate doubles, because I think the random function in PHP is also not truly unique. So you might want to add something even more unique to the code, like a time based value. All depends on how long your code can be.
$table = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$code = "";
srand(66);
for ($i = 0; $i < 8; $i++) {
$code .= mb_substr($table, rand(0, mb_strlen($table) - 1), 1);
}
:: Second, the page that handles the rewritten url with id -which will handle the url with that code also- would first have to check if the id (which might be an id, but also a code) is in the post_meta table. And if so, get the post that matches that code.

GA4 + GTM: Remove URL query params from all page data

How do I remove URL params from getting pushed to GA4 through GTM? I have tried many solutions but none seem to be working.
Which key in "Fields to Set" do I need to use so GTM replaces the url query param from all dimensions like page_path, page_location, path_referrer?
This article has been my life saver when dealing with URL params in GA4, but please use my experience and avoid the mistake of applying the script directly to page_location.
page_location is what I call a technical dimension that GA4 uses to sort referring websites according to its internal rules and do any other GA4 things. Remove URL params from page_location using GTM, and you'll stop seeing all channels, reliant on UTMs—so paid search, display, paid social, email etc (provided you use UTMs, of course). Don't forget: in this case, you remove the URL params in GTM before they get in GA, so if GTM strips params out, GA doesn't see them.
To illustrate my mistake, this is how my GA4 configuration tag in GTM looked like initially:
Bad idea. Don't touch page_location.
The best approach is to just create your own dimension which you would use to store 'clean' URLs, say, page_URI. The reason: you stop relying on GA built-in dimensions that (potentially) are prone to change and you create something of your own that you will have control over and can add to any event as a dimension.
Below is my version of the script in GTM, deployed as a Custom Javascript Variable:
function() {
var params = ['hash', 'email', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'gclid', 'fbclid', 'mc_cid', 'mc_eid', 'msclkid']; //Add URL params to be excluded from page URI
var a = document.createElement('a');
var param,
qps,
iop,
ioe,
i;
a.href = {{Page URL}};
if (a.search) {
qps = '&' + a.search.replace('?', '') + '&';
for (i = 0; i < params.length; i++) {
param = params[i];
iop = qps.indexOf('&' + param + '=');
if(iop > -1) {
ioe = qps.indexOf('&', iop + 1);
qps = qps.slice(0, iop) + qps.slice(ioe, qps.length);
}
}
a.search = qps.slice(1, qps.length - 1);
}
return a.href;
}
Two things to mention about the code:
List all params you want to strip out in the array params;
a.href = {{Page URL}} - the code makes use of GTM's built-in variable Page URL (hence double curly brackets) that captures the full URL (without hostname, though). If you feel fancy, you can replace it with plain JS.
So the code above now populates the GTM field/GA4 dimension page_URI in the main configuration tag and any other tags, where I think having a clean URI is useful:
I do realize that this approach uses up one GA4 dimension, but it's a price I'm willing to pay to have a clean URL in the absence of a better solution.
In the GA4 tag in GTM try to set page_location as Field to Set and a Custom JavaScript Variable as value with this definition:
function(){
return document.location.hostname + document.location.pathname;
}
i.e. (note: App+Web is old name of GA4):
You can also use the following JavaScript in the custom JavaScript variable instead of the custom JavaScript mentioned above.
In this custom JavaScript instead of creating a new anchor element, we simply are taking the full page URL and then using the JavaScript's in-built URL() method to convert it to a proper URL that can be programmatically managed and then manage it according to the need.
I'm sharing my script below:
Step 1
Create a custom JavaScript variable inside your GTM and add the following JavaScript code into it.
function() {
// Set the array with the list of query string you would like to remove being shown up in Google Analytics 4
var excuded_query_params = [
'add',
'the',
'query',
'strings',
'you',
'would',
'like',
'to',
'be',
'removed',
'from',
'GA4',
'analytics',
'report'
]
// Get the full Page URL from GTM in-build variables
var page_url_string = {{Page URL}}
// Convert the received URL from string format to URL format
var page_url = new URL( page_url_string )
var page_url_copy = new URL( page_url_string )
// Loop through the query parameters in the URL and if there is any query param which is in the excluded list,
// remove that from the full URL
page_url_copy.searchParams.forEach( function(param_value, param_name) {
if( excuded_query_params.includes( param_name ) ) {
page_url.searchParams.delete( param_name )
}
} )
// Return the final URL
return page_url.toString()
}
Please Note: as we are going to replace the value of page_location a default GA4 variable's data - it is highly recommended that you do not remove the utm_ query parameters from the URL as GA4 reports use that data internally and that may lead to report breaking. So, it's best that you do not remove query parameters like utm_souyrce, utm_campaign etc.
Step 2
Inside your GA4 Configuration Tag, click on Fields to Set and add a new field with the Field Name set as page_location and value set as this custom JavaScript variable.
Step 3
Now it's time to preview inside GTM and deeply.

How to create an advanced search/filter using Google App Maker's Query Script?

I'm making an app with an advanced search feature which can help users filter data from dropdowns and textboxes (Dropdown to choose column and clause, Textbox for entering search parameter) like this one:
Advanced Search page sample:
I tried to bind the Column's name dropdown to #datasource.query.parameters.Parameter and changed the Query part of the datasource like this:
Datasource's Query Script and Parameters:
However, I keep getting errors like:
Parameter 'Column' is used in 'where' clause but not defined in property 'parameters'
Could you please tell me how can I resolve this problem?
You literally have to construct your 'Where' clause in this case and then set the parameter of the where clause equal to your parameters. So lets say your first set of parameters is Column1: Name, Query1: contains, Parameter1: John, then your datasource needs to have the following parameters Column1, Query1, and Parameter1 and your bindings on your dropdown1, dropdown2, and textbox1 should be:
#datasource.query.parameters.Column1
#datasource.query.parameters.Query1
#datasource.query.parameters.Parameter1
respectively.
Then your query script needs to be as follows:
if (query.parameters.Field1 === null || query.parameters.Query1 === null) {
throw new app.ManagedError('Cannot complete query without Parameters!');
}
switch (app.metadata.models.MaintenanceManagement.fields[query.parameters.Field1].type) {
case 'Number':
query.parameters.Parameter1 = Number(query.parameters.Parameter1);
break;
case 'Date':
query.parameters.Parameter1 = new Date(query.parameters.Parameter1);
break;
case 'Boolean':
if (query.parameters.Parameter1 === 'True' || query.parameters.Parameter1 === 'true') {
query.parameters.Parameter1 = true;
} else {
query.parameters.Parameter1 = false;
}
break;
default:
query.parameters.Parameter1 = query.parameters.Parameter1;
}
query.where = query.parameters.Column1 + " " + query.parameters.Query1 + "? :Parameter1";
return query.run();
So your where statement essentially becomes a string that reads 'Name contains? :Parameter1' (i.e. John) that then becomes your query. Hope this makes sense, feel free to ask follow up questions.

How to check for absolute paths/routes in Symfony?

First of all thank you for taking your time takling this difficult topic.
Goal:
My URL structure is like facebook/twitter: site.com/Username
To let the user set the username, I need to check if a path of a route matches a username.
Possibilities & problems:
If somebody is interested, the possible solutions I came up with:
1.) Using getRouteCollection()
Problem: This method shouldn't be used because it rebuilds the route cache and kills performance (see https://github.com/symfony/symfony-docs/issues/6710)
2.) Using a blacklist as RegExp in the User (for Username) Entity to blacklist all sites like "settings", "about", "login", "register"...
* #Assert\Regex(pattern="/^(?!register)(?!login)...
Problem: This is nearly guaranteed to explode because sometimes a URL is forgotten to add into this RegExp
3.) Using CURL to check if the site.com/username results in a 404
Problem: This seems to be a "hack" for me and unprofessional.
Question:
Whats the professional, secure way of checking if a route doesn't already exists, so that the username can be set and is guaranteed to be "free" (via absolute path like site.com/Username and no other route is in the way)?
By example something like $app->hasURL('/username')
Add: use \Symfony\Component\RoutingException\ResourceNotFoundException;
Then, where $router is the routing service, you can attempt to match the url to a route. If it doesn't match anything it will throw an exception, but if you have a default catch-all route it will always match something so you would need to just detect if it matches the catch-all instead:
$routeIsAvailable = false;
try {
$route = $router->match( '/' . $username );
// If you are using a catch-all route to load profiles
if ( $route[ '_route' ] === 'name_of_catch_all_route' )
$routeIsAvailable = true;
} catch ( ResourceNotFoundException ) {
// If the url didn't match any route at all
$routeIsAvailable = true;
}
Make sure that you validate $username first so that it doesn't have any characters like ? or /. You can also urlencode it, but you probably don't want your user's profile urls to have encoded characters in them, so it's better to just restrict the valid usernames.

Include "Change Note" when creating content from InvokeFactory

I am creating a content item from a PloneFormGen Form Custom Script Adapter using invokeFactory. Everything is working fine so far, however we want to start generating a comment to be included in the create action, for the history of the item. The comment itself will be generated using fields from the form and some preset text.
Is this something that would be possible from PFG?
The content type is a custom type, and it is versionable. Using Plone 4.3.2, PFG 1.7.14
EDIT
My current code:
from Products.CMFPlone.utils import normalizeString
portal_root = context.portal_url.getPortalObject()
target = portal_root['first-folder']['my-folder']
form = request.form
title = "My Title: "+form['title-1']
id = normalizeString(title)
id = id+"_"+str(DateTime().millis())
target.invokeFactory(
"MyCustomType",
id=id,
title=title,
text=form['comments'],
relatedItems=form['uid']
)
I have tried using keys like comments, comment, message, and even cmfeditions_version_comment within the target.invokeFactory arguments. No luck so far.
I'm not sure if that's possible in a custom script adapter.
The action of you first entry is None. The history automatically shows Create if the action is None. This is implemented here (plone.app.layout.viewlets.content)
# On a default Plone site you got the following
>>> item.workflow_history
{'simple_publication_workflow': ({'action': None, 'review_state': 'private', 'actor': 'admin', 'comments': '', 'time': DateTime('2014/10/02 08:08:53.659345 GMT+2')},)}
Key of the the dict is the workflow id and the value is a tuple of all entries.
So you can manipulate the entry like you want. But I don't know if this is possible with restricted python (custom script adapter can only use restricted python).
But you could also add a new entry, by extending you script with:
...
new_object = target.get(id)
workflow_tool = getToolByName(new_object, 'portal_workflow')
workflows = workflow_tool.getWorkflowsFor(new_object)
if not workflows:
return
workflow_id = workflows[0].id # Grap first workflow, if you have more, take the the one you need
review_state = workflow_tool.getInfoFor(new_object, 'review_state', None)
history_entry = {
'action' : action, # Your action
'review_state' : review_state,
'comments' : comment, # Your comment
'actor' : actor, # Probably you could get the logged in user
'time' : time,
}
workflow_tool.setStatusOf(workflow_id, context, history_entry)

Resources