Auto generating SilverStripe admin links - admin

So I have a DataObject that I manage via Model Admin via the standard gridfield (using Silverstripe 4)
I would like to be able to generate a link to edit this object directly the admin. I can do this if I manually enter the ModelAdmin URL, but this doesn't seem very dynamic (if we change admin URL's in the future then that approach will break).
I have tried to implement the below:
class MyObject extends DataObject
{
public function EditLink()
{
$classname = str_replace('\\', '-', $this->owner->ClassName);
$admin = OrderAdmin::singleton();
$fields = $admin->getEditForm()->Fields();
$grid = $fields
->fieldByName($classsname);
return Controller::join_links(
$grid->Link("item"),
$this->owner->ID,
"edit"
);
}
}
But this outputs this error & stacktrace:
[Emergency] Uncaught InvalidArgumentException: SilverStripe\ORM\DataObject is not a subclass of DataObject
POST /as-plumbing-app-v2/admin/jobs/Job/EditForm/field/Job/item/1/ItemEditForm/
Line 147 in /home/morven/Projects/as-plumbing-app-v2/vendor/silverstripe/framework/src/ORM/DataObjectSchema.php
Source
138 {
139 $class = ClassInfo::class_name($class);
140 $current = $class;
141 while ($next = get_parent_class($current)) {
142 if ($next === DataObject::class) {
143 return $current;
144 }
145 $current = $next;
146 }
147 throw new InvalidArgumentException("$class is not a subclass of DataObject");
148 }
149
150 /**
151 * Get the base table
152 *
153 * #param string|object $class
Trace
SilverStripe\ORM\DataObjectSchema->baseDataClass(SilverStripe\ORM\DataObject)
DataQuery.php:165
SilverStripe\ORM\DataQuery->initialiseQuery()
DataQuery.php:76
SilverStripe\ORM\DataQuery->__construct(SilverStripe\ORM\DataObject)
DataList.php:61
SilverStripe\ORM\DataList->__construct(SilverStripe\ORM\DataObject)
ReflectionClass->newInstanceArgs(Array)
InjectionCreator.php:23
SilverStripe\Core\Injector\InjectionCreator->create(SilverStripe\ORM\DataList, Array)
Injector.php:585
SilverStripe\Core\Injector\Injector->instantiate(Array, SilverStripe\ORM\DataList, prototype)
Injector.php:988
SilverStripe\Core\Injector\Injector->getNamedService(SilverStripe\ORM\DataList, , Array)
Injector.php:941
SilverStripe\Core\Injector\Injector->get(SilverStripe\ORM\DataList, , Array)
Injector.php:1111
SilverStripe\Core\Injector\Injector->createWithArgs(SilverStripe\ORM\DataList, Array)
Injectable.php:30
SilverStripe\View\ViewableData::create(SilverStripe\ORM\DataObject)
SearchContext.php:160
SilverStripe\ORM\Search\SearchContext->getQuery(Array, , )
SearchContext.php:210
SilverStripe\ORM\Search\SearchContext->getResults(Array)
ModelAdmin.php:347
SilverStripe\Admin\ModelAdmin->getList()
OrderAdmin.php:166
SilverCommerce\OrdersAdmin\Admin\OrderAdmin->getList()
ModelAdmin.php:184
SilverStripe\Admin\ModelAdmin->getEditForm(, )
OrderAdmin.php:88
SilverCommerce\OrdersAdmin\Admin\OrderAdmin->getEditForm()
Estimate.php:151
SilverCommerce\OrdersAdmin\Model\Estimate->EditLink()
JobDetailForm_ItemRequest.php:117
App\Forms\GridField\JobDetailForm_ItemRequest->doViewInvoice(Array, SilverStripe\Forms\Form, SilverStripe\Control\HTTPRequest, SilverStripe\Forms\FormRequestHandler)
FormRequestHandler.php:231
SilverStripe\Forms\FormRequestHandler->httpSubmission(SilverStripe\Control\HTTPRequest)
RequestHandler.php:320
SilverStripe\Control\RequestHandler->handleAction(SilverStripe\Control\HTTPRequest, httpSubmission)
RequestHandler.php:201
SilverStripe\Control\RequestHandler->handleRequest(SilverStripe\Control\HTTPRequest)
RequestHandler.php:225
SilverStripe\Control\RequestHandler->handleRequest(SilverStripe\Control\HTTPRequest)
GridFieldDetailForm.php:118
SilverStripe\Forms\GridField\GridFieldDetailForm->handleItem(SilverStripe\Forms\GridField\GridField, SilverStripe\Control\HTTPRequest)
GridField.php:1019
SilverStripe\Forms\GridField\GridField->handleRequest(SilverStripe\Control\HTTPRequest)
RequestHandler.php:225
SilverStripe\Control\RequestHandler->handleRequest(SilverStripe\Control\HTTPRequest)
RequestHandler.php:225
SilverStripe\Control\RequestHandler->handleRequest(SilverStripe\Control\HTTPRequest)
Controller.php:207
SilverStripe\Control\Controller->handleRequest(SilverStripe\Control\HTTPRequest)
LeftAndMain.php:750
SilverStripe\Admin\LeftAndMain->handleRequest(SilverStripe\Control\HTTPRequest)
AdminRootController.php:123
SilverStripe\Admin\AdminRootController->handleRequest(SilverStripe\Control\HTTPRequest)
Director.php:360
SilverStripe\Control\Director->SilverStripe\Control\{closure}(SilverStripe\Control\HTTPRequest)
VersionedHTTPMiddleware.php:40
SilverStripe\Versioned\VersionedHTTPMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
AuthenticationMiddleware.php:61
SilverStripe\Security\AuthenticationMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
CanonicalURLMiddleware.php:155
SilverStripe\Control\Middleware\CanonicalURLMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
FlushMiddleware.php:26
SilverStripe\Control\Middleware\FlushMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
RequestProcessor.php:66
SilverStripe\Control\RequestProcessor->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
SessionMiddleware.php:20
SilverStripe\Control\Middleware\SessionMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
AllowedHostsMiddleware.php:60
SilverStripe\Control\Middleware\AllowedHostsMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
TrustedProxyMiddleware.php:176
SilverStripe\Control\Middleware\TrustedProxyMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\Director->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
HTTPMiddlewareAware.php:65
SilverStripe\Control\Director->callMiddleware(SilverStripe\Control\HTTPRequest, Closure)
Director.php:369
SilverStripe\Control\Director->handleRequest(SilverStripe\Control\HTTPRequest)
HTTPApplication.php:48
SilverStripe\Control\HTTPApplication->SilverStripe\Control\{closure}(SilverStripe\Control\HTTPRequest)
call_user_func(Closure, SilverStripe\Control\HTTPRequest)
HTTPApplication.php:66
SilverStripe\Control\HTTPApplication->SilverStripe\Control\{closure}(SilverStripe\Control\HTTPRequest)
call_user_func(Closure, SilverStripe\Control\HTTPRequest)
ErrorControlChainMiddleware.php:56
SilverStripe\Core\Startup\ErrorControlChainMiddleware->SilverStripe\Core\Startup\{closure}(SilverStripe\Core\Startup\ErrorControlChain)
call_user_func(Closure, SilverStripe\Core\Startup\ErrorControlChain)
ErrorControlChain.php:236
SilverStripe\Core\Startup\ErrorControlChain->step()
ErrorControlChain.php:226
SilverStripe\Core\Startup\ErrorControlChain->execute()
ErrorControlChainMiddleware.php:69
SilverStripe\Core\Startup\ErrorControlChainMiddleware->process(SilverStripe\Control\HTTPRequest, Closure)
HTTPMiddlewareAware.php:62
SilverStripe\Control\HTTPApplication->SilverStripe\Control\Middleware\{closure}(SilverStripe\Control\HTTPRequest)
HTTPMiddlewareAware.php:65
SilverStripe\Control\HTTPApplication->callMiddleware(SilverStripe\Control\HTTPRequest, Closure)
HTTPApplication.php:67
SilverStripe\Control\HTTPApplication->execute(SilverStripe\Control\HTTPRequest, Closure, )
HTTPApplication.php:49
SilverStripe\Control\HTTPApplication->handle(SilverStripe\Control\HTTPRequest)
index.php:17
I am assuming that this is because I have not passed the correct response object to ModelAdmin when I created it, so it is unsure what ModelClass to use?
Does anyone have any ideas on the preferred way to implement this?

This should work for SilverStripe 3 and 4 (Tested on 3.6 and 4.0)
class MyObject extends DataObject
{
public function EditLink()
{
$classname = str_replace('\\', '-', singleton($this->owner->ClassName)->i18n_plural_name());
$admin = OrderAdmin::singleton();
$fields = $admin->getEditForm()->Fields();
$grid = $fields->dataFieldByName($classname);
return Controller::join_links(
$grid->Link("item"),
$this->owner->ID,
"edit"
);
}
}
Changes made to your original include
Using plural version of ClassName
Fixing typo of $classname when requesting Grid
using ->dataFieldByName() rather than ->fieldByName()
NOTE: SilverStripe 4.0 uses proper namespacing, so you also want to ensure you are using the correct namespace for DataObject.
E.g.
namespace MySite;
use SilverStripe\ORM\DataObject;
class MyObject extends DataObject
{
...

Related

Creating index with alias

Sorry for the rudimentary question.I'm developing PHP small test class that manipulates AWS Elasticsearch Service.
<?php
namespace MyCompany\Aws;
use Elasticsearch\ClientBuilder;
class ElasticsearchApi {
private $client;
function __construct(string $hosts='', string $username='', string $password=''){
... //configure $this->client
}
/**
* Create index
* #param string $index
* #param array $body
* #return bool
*/
public function createIndex(string $index, array $body = []):bool{
$request = array(
'index' => $index,
'body' => $body
);
$ret = $this->client->indices()->create($request);
return $ret['acknowledged'];
}
}
And call createIndex function specifying alias as described at:
https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html
<?php
require 'vendor/autoload.php';
use MyCompany\Aws\ElasticsearchApi as ES;
$es = new ES();
$body = array('aliases'=>array(
'actions'=>array(
'add'=>array(
'alias'=>'sample-alias',
'index'=>'test-index'
)
)
));
$ret = $es->createIndex('test-index',$body);
var_dump($ret);
This operation normally ends:
PS D:\My_Documents\Proj\Elasticsearch\index-gen> php es-lib-test.php
bool(true)
PS D:\My_Documents\Proj\Elasticsearch\index-gen>
However the generated index does not seem to have alias.
What is wrong with createIndex parameter? I'm using version 7.8 of Elasticsearch on AWS.
Addendum
Based on the #Val 's suggestion, I changed the code as follows:
$body = array(
'aliases'=>array(
'sample-alias'=>array()
)
);
$ret = $es->createIndex('test-index',$body);
var_dump($ret);
However I got the following exception. Are there any mistakes?
PS D:\My_Documents\Proj\Elasticsearch\index-gen> php es-lib-test.php
PHP Fatal error: Uncaught Elasticsearch\Common\Exceptions\BadRequest400Exception: {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"No alias is specified"}],"type":"illegal_argument_exception","reason":"No alias is specified"},"status":400} in D:\My_Documents\Proj\Elasticsearch\index-gen\vendor\elasticsearch\elasticsearch\src\Elasticsearch\Connections\Connection.php:641
Stack trace:
#0 D:\My_Documents\Proj\Elasticsearch\index-gen\vendor\elasticsearch\elasticsearch\src\Elasticsearch\Connections\Connection.php(328): Elasticsearch\Connections\Connection->process4xxError(Array, Array, Array)
#2 D:\My_Documents\Proj\Elasticsearch\index-gen\vendor\ezimuel\ringphp\src\Future\CompletedFutureValue.php(55): React\Promise\FulfilledPromise->then(Object(Closure), NULL, NULL)
#3 D:\My_Documents\Proj\Elasticsearch\index-gen\vendor\ezimuel\ring in D:\My_Documents\Proj\Elasticsearch\index-gen\vendor\elasticsearch\elasticsearch\src\Elasticsearch\Connections\Connection.php on line 641
Your body should simply look like this:
$body = array('aliases' => array('sample-alias' => array()));
The syntax you're using is for the _aliases endpoint not for the index creation one.

GetSymbolInfo().Symbol returning null on AttributeSyntax

I have a custom attribute I'm using to test a Roslyn code analyzer I'm writing:
[AttributeUsage( validOn: AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = true )]
public class DummyAttribute : Attribute
{
public DummyAttribute( string arg1, Type arg2 )
{
}
public int TestField;
}
which decorates another test class:
[Dummy( "", typeof( string ) )]
[Dummy( "test", typeof( int ) )]
[Dummy( "test", typeof( int ) )]
public class J4JLogger<TCalling> : IJ4JLogger<TCalling>
{
But when I call GetSymbolInfo() on it with the semantic model it's defined in:
model.GetSymbolInfo( attrNode ).Symbol
the value of Symbol is null.
What's odd is that the GetSymbolInfo() call works perfectly well for attribute classes defined in the Net Core library (e.g., AttributeUsage).
Both Dummy and J4JLogger are defined in the same project. I create my compilation unit by parsing the files individually (e.g., J4JLogger is parsed and analyzed separately from Dummy) so when I'm parsing J4JLogger there is no reference to the assembly containing both J4JLogger and Dummy.
Could the problem be that model doesn't actually contain the Dummy class I think it does? Is there a way to check what's in the semantic model? Do I have to include a reference to the assembly whose source file I'm analyzing in the semantic model?
Corrected Parsing Logic
My original parsing logic parsed each file into a syntax tree independent of all its sister source files. The correct way to parse source files -- at least when they depend on each other -- is something like this:
protected virtual (CompilationUnitSyntax root, SemanticModel model) ParseMultiple( string primaryPath, params string[] auxPaths )
{
if( !IsValid )
return (null, null);
CSharpCompilation compilation;
SyntaxTree primaryTree;
var auxFiles = auxPaths == null || auxPaths.Length == 0
? new List<string>()
: auxPaths.Distinct().Where( p => !p.Equals( primaryPath, StringComparison.OrdinalIgnoreCase ) );
try
{
var auxTrees = new List<SyntaxTree>();
primaryTree = CSharpSyntaxTree.ParseText( File.ReadAllText( primaryPath ) );
auxTrees.Add( primaryTree );
foreach( var auxFile in auxFiles )
{
var auxTree = CSharpSyntaxTree.ParseText( File.ReadAllText( auxFile ) );
auxTrees.Add( auxTree );
}
compilation = CSharpCompilation.Create( ProjectDocument.AssemblyName )
.AddReferences( GetReferences().ToArray() )
.AddSyntaxTrees( auxTrees );
}
catch( Exception e )
{
Logger.Error<string>( "Configuration failed, exception message was {0}", e.Message );
return (null, null);
}
return (primaryTree.GetCompilationUnitRoot(), compilation.GetSemanticModel( primaryTree ));
}
A minor gotcha is that AddSyntaxTrees() does not appear to be incremental; you need to add all the relevant syntax trees in one call to AddSyntaxTrees().
Turns out the problem was that you have to include all the source files that reference each other (e.g., Dummy and J4JLogger in my case) in the compilation unit because otherwise the "internal" references (e.g., decorating J4JLogger with Dummy) won't resolve. I've annotated the question with how I rewrote my parsing logic.

What is the best practice for repository?

In my repositories, I have methods with too many arguments (for use in where) :
Example :
class ProchaineOperationRepository extends EntityRepository
{
public function getProchaineOperation(
$id = null, // Search by ID
\DateTime $dateMax = null, // Search by DateMax
\DateTime $dateMin = null, // Search by DateMin
$title = null // Search by title
)
In my controllers, I have differents action ... for get with ID, for get with ID and DateMin, for get ID and Title, ...
My method is too illegible because too many arguments ... and it would be difficult to create many methods because they are almost identical ...
What is the best practice ?
You have two main concerns in your question
You have too many arguments in your repository method which will be used in 'where' condition of the eventual query. You want to organize them in a better way
The repository method should be callable from the controller in a meaningful way because of possible complexity of arguments passed
I suggest you to write a Repository method like:
namespace AcmeBundle\Repository;
/**
* ProchaineOperationRepository
*
*/
class ProchaineOperationRepository extends \Doctrine\ORM\EntityRepository
{
public function search($filters, $sortBy = "id", $orderBy = "DESC")
{
$qb = $this->createQueryBuilder("po");
foreach ($filters as $key => $value){
$qb->andWhere("po.$key='$value'");
}
$qb->addOrderBy("po.$sortBy", $orderBy);
return $qb->getQuery()->getArrayResult();
}
}
The $filters variable here is an array which is supposed to hold the filters you are going to use in 'where' condition. $sortBy and $orderBy should also be useful to get the result in properly sequenced way
Now, you can call the repository method from your controller like:
class ProchaineOperationController extends Controller
{
/**
* #Route("/getById/{id}")
*/
public function getByIdAction($id)
{
$filters = ['id' => $id];
$result = $this->getDoctrine()->getRepository("AcmeBundle:ProchaineOperation")->search($filters);
//process $result
}
/**
* #Route("/getByTitle/{title}")
*/
public function getByTitleAction($title)
{
$filters = ['title' => $title];
$sortBy = 'title';
$result = $this->getDoctrine()->getRepository("AcmeBundle:ProchaineOperation")->search($filters, $sortBy);
//process $result
}
/**
* #Route("/getByIdAndDateMin/{id}/{dateMin}")
*/
public function getByIdAndDateMinAction($id, $dateMin)
{
$filters = ['id' => $id, 'dateMin' => $dateMin];
$sortBy = "dateMin";
$orderBy = "ASC";
$result = $this->getDoctrine()->getRepository("AcmeBundle:ProchaineOperation")->search($filters, $sortBy, $orderBy);
//process $result
}
}
Note that you are calling the same repository method for all controller actions with minor changes according to your parameters. Also note that $sortBy and $orderBy are optionally passed.
Hope it helps!
If your objective is only to query with an AND operator between each properties, the best way could be to use the method proposed by doctrine for that : findBy() cf : this part of the doc
for instance :
$results = $this
->getDoctrine()
->getRepository('AppBundle:ProchaineOperation')
->findBy(array('dateMax' => $myDate, 'title' => 'Hello world');
EDIT : after comment
Then use the same way as Doctrine do : Pass only an array with id, dateMax... as keys if these are set. This should be solve the method signature problem which gives you so much trouble. :)

Mocking grails method that uses a findAll, Generating a MissingMethodException

def retrieveEatenFood(String token, String addedDate) {
def consumer = Consumer.findByMobileToken(token)
if(consumer != null) {
def efList = []
def list = consumer.findAll("from EatenFood as ef where date(ef.dateAdded) = date(:da)",[da:sdf_long.parse(addedDate)])
list.each{
def eatenList = [:]
eatenList.put("foodType",it.food.name)
eatenList.put("sequenceNumber",it.sequenceNumber)
eatenList.put("eatenDate", it.eatenDate)
eatenList.put("DateAdded",it.dateAdded)
efList.add(eatenList);
}
return efList;
}
}
Trying to mock the above method, but the findAll keep generating an exception.
This issue it works! Now i need to write the test for it and i keep getting this exception. Can anyone help me please!
groovy.lang.MissingMethodException: No signature of method: carrotdev.Consumer.findAll() is applicable for argument types: (java.lang.String, java.util.LinkedHashMap) values: [from EatenFood as ef where date(ef.dateAdded) = date(:da), [da:Sun Feb 13 01:51:47 AST 2011]]
Possible solutions: findAll(groovy.lang.Closure), find(groovy.lang.Closure)
at carrotdev.ConsumerService.retrieveEatenFood(ConsumerService.groovy:146)
at carrotdev.ConsumerService$retrieveEatenFood.call(Unknown Source)
at carrotdev.ConsumerServiceTests.testEatenFoodRetrievedSucessfully(ConsumerServiceTests.groovy:359)
I would move the query to the Consumer domain class with a descriptive name, e.g.
static List<EatenFood> findAllEatenByDate(String date) {
consumer.findAll(
"from EatenFood as ef where date(ef.dateAdded) = date(:da)",
[da:sdf_long.parse(addedDate)])
}
Then the call is simply
def list = Consumer.findAllEatenByDate(addedDate)
and you can mock that easily with
def foods = [new EatenFood(...), new EatenFood(...), ...]
Consumer.metaClass.static.findAllEatenByDate = { String date - > foods }
Be sure to test the finder method in your Consumer integration test.

Jade templating "each" function returns empty object

I've had a bug that has been bugging me for days. I'm pretty new to Node and the Jade templating system so bear with me: I'm looking to add stylesheets in the following way:
App.js (Express):
app.get('/', loadUser, function(req, res) {
var User = req.user;
// console.log(User.groups[2]);
// var groups = User.groups.split(',');
// OK DUh. This only gets called when the client has the script Socket.IO
// and client runs socket.connect()
getMessages(User, function(messages) {
var locals = {
scripts: [
'https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js',
'index.js'
],
stylesheets: [
'index.css'
],
user : User,
messages: messages
};
console.log('ok');
res.render('app.jade', {locals : locals});
});
});
In layout.jade (which is executed with app.jade) I have:
!!! 5
html
head
title UI
link(rel='stylesheet', href = 'stylesheets/reset.css')
link(rel='stylesheet', href = 'stylesheets/layout.css')
- var stylesheets = stylesheets || [];
#{stylesheets}
- each stylesheet in stylesheets
- if(stylesheet.indexOf('http') >= 0)
link(rel='stylesheet', href = stylesheet)
- else
link(rel='stylesheet', href = "stylesheets/"+stylesheet )
Plus more... I keep running into the same error:
9. ' - if(stylesheet.indexOf(\'http') >= 0)'
Object function () {
var o = {}, i, l = this.length, r = [];
for(i=0; i
for(i in o) r.push(o[i]);
return r;
} has no method 'indexOf'
Now.. the gotcha is that this exact template works in another application that passes in the exact same variables: I would REALLY appreciate any suggestions you guys have on this thorny issue!
Thanks!
Matt Mueller
So here's your issue...
in this line:
res.render('app.jade', {locals : locals});
you are passing in locals ==> locals, which is a hash (ok, so I'm a PERL guy, I think JS calls them 'associative arrays')
So now inside your jade template we have the line:
- var stylesheets = stylesheets || [];
inside JADE, you have defined the variable "locals", but everything else is hidden under that, so the variable "stylesheets" is NOT defined (locals.stylesheets is defined instead). So this line of code sets the variable "stylesheets" to "[]"
So here's where I have to speculate. "indexOf" is a method of the Array object. Perhaps arrays constructed inside JADE don't have this method whereas arrays constructed in node.js DO have this method. Which would explain why you get an error trying to call "stylesheets.indexOf(...)"