Sulu: how to make custom entity translatable? - entity

So I have my custom entity type, created it by following official tutorial:
https://docs.sulu.io/en/2.2/book/extend-admin.html
However entity I got is not translatable like i.e. standard pages or articles. I also didn't any info on how to make it translatable. Expected behavior is just to work as those standard types.
How to achieve that?

There are basically three things to do:
You have to add a new Translation entity for your custom entity. So if you have an Event entity, you need an additional EventTranslation entity. See https://github.com/sulu/sulu-workshop/tree/master/src/Entity
You need to tell Sulu, that your custom entity is translatable by adding the available locales to the view in your AppAdmin class, see https://github.com/sulu/sulu-workshop/blob/master/src/Admin/EventAdmin.php#L74
You need to adjust your custom entity's admin controller (it will receive a locale request parameter now) to persist the localized properties to the CustomEntityTranslation instead of the CustomEntity iself, see https://github.com/sulu/sulu-workshop/blob/master/src/Controller/Admin/EventController.php
So as conclusion, Sulu is only responsible for showing the locale switcher in the upper right corner and appending the current selected locale as locale parameter to your api calls. Everything else is completely up to you, you have to implement that like in a normal symfony application

Related

How to register two controllers same name, different namespace in same ODATA EDM Model?

Objective
The objective is to better understand how to develop modular (plugins by 3rd parties) approach to developing APIs, on top of ASP.NET Core.
To make plugin development easy to pick up it should rely on Conventions where possible, and deliver high value.
OData's queryability and being an Standard remains a compelling improvement to REST, providing a lot of bang for effort.
Issues
OData may be powerful, but the availability of current documentation remains sub par, hence unsure of its limits/capabilities.
Hence questions regarding what options one has to untangling routing issues due to conflicting endpoints from controllers in 3rd party modules.
Specifically:
Q1: Can a single OData EDM model disambiguate between two Controllers with the same name in two different namespaces?
Q2: Can one register an ODataController with a different Route than the ODataController name (eg route Foo points to BarController, when the convention is it would look for a FooController) without breaking default functionality? (eg: $count stops working for me)
Q2b: Even if we take over parsing incoming Uris into its odata path components as well as the logic it uses to find the relevant Controller?
Q3: If it can't, would the more practical approach to loading Plugins be for each Plugin to register its own EDM models?
Q4: Will using multiple EDM models work even if the second plugin/edm model has Controllers that exposes Models that has properties that refer back to Models exposed by Controllers in the base (or another dependency Plugin) EDM model?
Eg: Base EDM exposes Persons and Addresses, and second plugin EDM exposes maybe Customers, which has properties referencing Person and Addresses types, provided by the base EDM?
(I'm guessing it will work, but not 100% sure...anybody see an issue with this?)
Q5: How can one add new EDM models dynamically, resetting the routes?
For example if one uploaded a nuget package that was a plugin, and it contained its own EDM model describing its Models and Controllers...all this happening way after after.Run() was invoked, how can one kick the system in the head to refind/relearn what are valid routes?
Q6: Why is $count only available on ODataControllers that are registered by Convention, but doesn't work on those registered by RouteAttribute?
Background
The latest(?) documentation I found in a blog post here:
https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/
But it's just a blog post, and a lot remains missing to understand all the pieces and how they fit together.
Process So Far
I'm listing my notes below to help others see better what I am trying, in case someone can see what I am obviously getting wrong.
And if it helps and you want working code, I've uploaded my investigation efforts (actually thrashingArounds):
https://github.com/skysgh/Spikes.AspNetCore.ODataRouting
Setup
The default scenario in OData is to register EDM models with Controller Routes equal to the prefix of the name of the Controller.
Eg:
static string ODataPrefixWithSlash = "api/odata/v{version}/"
class SomeModelController : ODataController {...}
//is registered in an EDM model using convention of matching prefix of controller name:
var builder = new ODataConventionModelBuilder();
//to build a whole model:
builder.EntitySet<SomeModel>("SomeModel"); //This will get found
builder.EntitySet<SomeModel>("Renamed"); //Without further work causes 404 as route string != controller prefix
var edmModelA = builder.Build();
//and the model later registered as the source of OData routing info:
var mvcBuilder = builder.Services
.AddControllers()
.AddOData(
opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)
//Add Module/PluginA Routes:
.AddRouteComponents(AppAPIConstants.ODataPrefixWithSlash,edmModelA);
if one has enabled
builder.Services.AddSwaggerGen();
app.UseODataRouteDebug();
one can navigate to ~odata and see the controller's Endpoints listed as:
~api/odata/v{version}/SomeModel
~api/odata/v{version}/SomeModel/$count <- note, showing for now (will break later...why?!)
Controller Choice Process
An article exists that talks about how routing has been updated in OData v8 RC, relying on RouteAttribute
https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/
After a lot of messing around, appears one can use a different name but
only IF one starts the route with the same prefix used to register the EDM Model (eg: api/odata/v{version}/
[ODataAttributeRouting]
// Half Works
// because the route starts with the same base as what the model was registered under
// fools it(?) (where?) into accepting it as an Odata controller.
// a) listed in ~/$odata as an odata controller (under api/odata/v{version}
// b) acting as an Odata controller (returning odata wrapper in json)
// c) but no default queryability (doesn't accept or list /$count)
// as the route starts with same prefix
// as Convention used when registereing EDM model
// But $count doesn't work!
[Route(AppAPIConstants.ODataPrefixWithSlash + "Renamed5")]
public class ValuesA5Controller : ODataController{
[EnableQuery(PageSize = 100)]
[HttpGet("")]
[HttpGet("Get")]
public IActionResult Get()
{
return Ok(FakeDataBuilder.Get());
}
}
The above permits registering the ODataController in the EDM model as follows:
builder.EntitySet<SomeModel>("Renamed5");
But it only HALF works.
it is listed as an OData Controller in $odata. ok.
it is acting (mostly) as an ODataController in that it accepts most OData commands $select, $filter, etc.
But it is failing at offering $count for some reason:
~api/odata/v{version}/Renamed5 <- showing, same as it was doing for earlier SomeModelController
~api/odata/v{version}/Renamed5/$count <- not showing, even though endpoint decorated with [EnableQuery].
Options?
Option A: More Route information
I suspect that one could decorate the Get method with more routes to enable $count:
[EnableQuery(PageSize = 100)]
[HttpGet("")]
[HttpGet("Get")]
[HttpGet("$count") ??? <- really???
public IActionResult Get() {....}
Certainly wish to avoid adding more codes as workaround (eg: what was tried here: https://stackoverflow.com/a/73042175/19926885)
but even if either worked, its more code, more sources of errors, etc. to watch out for.
If at all possible I'd like the least code, the most convention, while not locking in controller names.
Option B: Controller Selection process
As I said earlier, I don't know how or where the odata framework is magically matching "SomeModel" to "SomeModelController".
In an article I came across (https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/)
I saw mention of AttributeRoutingConvention: IODataControllerActionConvention which maybe could be put to use, but the blog post
didn't show when/how it could be registered, or replaced so have not been able to progress in that direction yet.
Also, iterating through registered Services I don't see anything inheriting from IODataControllerActionConvention. What's going on?
What I do see is:
//not sure yet what these do
IODataQueryRequestParser
IODataTemplateTranslator : Microsoft.AspNetCore.OData.Routing.Template.DefaultODataTemplateTranslator
IODataPathTemplateParser : Microsoft.AspNetCore.OData.Routing.Parser.DefaultODataPathTemplateParser`
But I don't have documentation on how they work, what they are for, etc.
Where next?
First of all THANK YOU for taking the time to read this long question!
Second, if you have advice as how I not thinking right as to how to solve the problem, that would help.
Third, if you are able to provide answers to the questions...wow. It's been a while I've looked for answers to this!
Took me a while to recognise the following basic misunderstandings on my part.
OData routing not something else but is an instance of the underlying WebAPI Routing
When one registers an EDM and providing the route prefix ("api/odata/v{version}"), it's just registering "~/api/odata/v{version}/~" as an action/route filter.
If you don't provide a [RouteAttribute] the convention is that it will make it up the Route segment from the Controller name (eg: 'PersonController' means that 'Person' will be used.
If you do provide a [RouteAttribute] on the Controller, you are saying this is the full (not a suffix!) route -- even if you provided a prefix when registering the EDM model.... To underline that point: just because HttpGet(...) acts as a suffix to the Route, doesn't meant the Route does too.
Which means that if it doesn't start with [Route("api/odata/v{version}")] -- maybe is just [Route("Renamed6")] -- it **doesn't match the route prefix used to register the edm model, so won't be handled by the models handler...so all it's doing is acting like all WebAPI controllers are doing and registers another route (whatever you gave), but won't consider it worthy of being OData queryability enabled....
So your route has to be [Route("api/odata/v{version}/Renamed6")].
Other than that, basics are that you'll get it confused if you register two models one with
"api/odata/v{version}", and another with
"api/odata/v{version}/plugin".
The first handler/whatever (i don't know what's behind the scenes here) will probably try to capture requests to plugin and not find a controller called "PluginController". I'm just guessing.
Finally, I think I saw issues with leaving dynamics tokens at the end for some reason. I had to change from using api/odata/PluginA/v{version} to api/odata/v{version}/PluginA . Not clear why.
Admittedly I still need to know a LOT more about the routing mechanism, but for now, this gets me forward again.
PS: Also...use Constants for building route strings. Turns out that Typos can really waste a lot of time :-(

How do I restrict which properties are bound using aspnetcore JSON from body of request

I could using [Bind("properties to include")] on an MVC action in ASPNET MVC 4. How do I restrict binding to specific properties using AspNetCore/MVC6? (RC2)
You could use JSON.NET's JsonIgnoreAttribute on properties which you want to exclude from deserialization, but note that this also makes the property not to serialize also. (Ideally you should have a model which is exactly what you expect from the user instead of ignoring certain properties, but I am not aware of how your requirements are)

Bind the request data to a model manually instead of it being in the action's parameters list

I have an action that handles different kinds (but similar) requests. So I need the request data to bind to different models depending on several external inputs.
Is there a way to do that (so that the model isn't in the action's parameters list but bound manually)?
Implementing your own IActionModelConvention realisation, you can change the parameter binding rules.
Article, that may help: Customising model-binding conventions in ASP.NET Core
Here and here are examples in MVC github repo.

Using DataTables.js with editor extension and ASP.NET MVC backend

I am using datatables with the editor extension to post data to an ASP.NET MVC backend. The action I am calling expects a complex object which I would like it to be able to automatically find the property values for using default model binding.
My problem is the default model binding expects the fields to have EXACTLY the same names as the public properties defined by my object. So when the table POSTs the data as data[name]="name" it can't find the value for the property 'Name'.
I can work around this problem by defining a custom model binder, but I have a lot of models I will be working with so would rather be able to reformat the data sent by the table so that the default model binder can find it e.g. name="name".
Can this be done?
Thanks
Nathan
Ok realise there's not much demand for this, but just incase the way to do it is to listen to the 'preSubmit' event and then manipulate the data object given in the parameter to match the structure and the names that you need on the server side.

Core Data returns NSManagedObject instead of Concrete class, but only when using . accessor

I have set up a Core Data model where I have two objects, say Person and Address. A person has an address, and an address can belong to many people. I have modelled it in core data as such (so the double arrow points to Person, while the single arrow goes to Address)
I have then created two classes for those objects, and implemented some custom methods in those classes. In the Core Data model I have entered the names of the classes into them.
If I fetch an Address from Core Data directly, it gives me the actual concrete class and I can call my custom methods on it.
If on the other hand I fetch a Person and try to access the Address through Person (eg: person.address) I get back an NSManagedObject that is an address (eg: I can get to all the core data attributes I've set on it) but it doesn't respond to my custom methods, because it's of type NSManagedObject instead of Address. Is this a limitation of Core Data or am I doing something wrong? If it is a limitation are there any work arounds?
Did you create those classes using the modeller (Select an Entity, File > new file.., Managed Object Class, then select the Model Entity)?
A while ago I had a similar problem because I didn't create my managed object models using the Modeller. What I did to make sure everything was up and running was to copy and save my custom methods (and everything else I'd implemented) and start from scratch using the modeller. Then I was able to customize my model classes again and everything worked just fine.
I know this is not a complete answer but perhaps it can help you until someone explains exactly what is going on.
Cheers!
You probably just forgot to set the name of the class in the model when you created the entity - it defaults to NSManagedObject. Click on Person and Address in the modeller and check, on the far right side where the Entity properties are listed, that the Class field is filled in correctly with the name of the corresponding objective C class and isn't just the default NSManagedObject setting.
Your implementation file for the class probably hasn't been added to the Target that you are running.
(Get Info on the .m file -> Check the targets tab)
If your xcdatamodel has the Class set, if it can't find it at run time it will still work, you will just get NSManagedObject instances back instead. Which will actually work just fine, until you try to add another method to the class, as you have found.