This is the official documentation of the forestadmin-agent-django and forestadmin-agent-flask Python agents.
In legacy agents declaring a Smart Action was a two-step process:
First, you had to declare by changing the parameters of the collection function in the appropriate app/forest/*.python file.
Then, you had to implement the action by creating a route handler in the appropriate app/urls.py/app/views.py file.
In the new agent, the process is simplified to a single step.
You can find the full documentation of action customization here.
Code cheatsheet
Legacy agent
New agent
Steps
Step 1: Calling add_action for the appropriate collection
Start by calling the add_action function on the appropriate collection and passing the appropriate parameters.
Most notably, you will need to pass:
type should become scope
Note that the values are now capitalized (e.g. single becomes Single)
Legacy agents defaulted to 'bulk' if no type was specified. The new agent requires you to specify the scope.
download should become generate_file. This is still a boolean and the same value can be passed.
endpoint and httpMethod should be removed. The agent will now automatically handle the routing.
from django_forest.utils.collection import CollectionclassCompanyForest(Collection):defload(self): self.actions = [{"type":"single","name":"Mark as Live"}, ]
from forestadmin.datasource_toolkit.decorators.action.context.single import ActionContextSinglefrom forestadmin.datasource_toolkit.decorators.action.result_builder import ResultBuilderdefmark_as_live(context: ActionContextSingle,result_builder: ResultBuilder):passagent.customize_collection("Company").add_action("Mark as live", {"scope": "Single","execute": mark_as_live, })
Step 2: Porting the form definition
Forms are now defined in the form property of the action.
You can simply copy the field's definition from the legacy agent to the new agent with the following differences:
fields should become form.
widget choice is no longer supported. A default widget will be used depending on the field type.
hook can be removed, those will be handled by the new agent automatically.
reference no longer exists. Use { type: "Collection", collection_name: '... } instead.
enums no longer exist. Use { type: "Enum", enum_values: ['...'] } instead.
agent.customize_collection("Customer").add_action("Charge credit card",{"scope": "Single","form": [ {"label": "amount","type": "Number","description": "The amount (USD) to charge the credit card. Example: 42.50","is_required": True } ],"execute": lambdactx, result_builder: pass})
Step 3: Porting the route to the new agent execute function
In the legacy agent, users had to implement the action by creating a route handler in the appropriate app/urls.py file.
This is no longer needed as the new agent provides a context object that contains all the information that is needed to implement the action.
When porting the route handler to the new agent, you will need to:
Move the body of the route handler to the execute function of the action.
Replace self.get_ids_from_request() call with context.get_record_ids().
Replace return JsonResponse(); calls with return result_builder.success() or return result_builder.error(), or the appropriate result_builder method.
from django.http import HttpResponsefrom app.model import CompanyclassMarksLiveAction(ActionView):defpost(self,request,*args,**kwargs): ids = self.get_ids_from_request(request, self.Model) company = Company.objects.filter(id__in=ids)[0] company.status ="live" company.save()returnHttpResponse(status=204)
from forestadmin.datasource_toolkit.interfaces.query.filter.unpaginated import Filterfrom forestadmin.datasource_toolkit.interfaces.query.condition_tree.nodes.leaf import ConditionTreeLeaffrom forestadmin.datasource_toolkit.decorators.action.context.single import ActionContextSinglefrom forestadmin.datasource_toolkit.decorators.chart.result_builder import ResultBuilderasyncdefmark_as_live(context: ActionContextSingle,result_builder: ResultBuilder): company_id =await context.get_record_id()await context.collection.update( { "status": 'live' },Filter( {"condition_tree": ConditionTreeLeaf("id", "equal", company_id)} ) )return result_builder.success('Company is now live!')agent.customize_collection('companies').addAction('Mark as Live', {"scope": "Single","execute": mark_as_live })
Step 4: Porting Smart Action hooks
Load hooks and change hooks have been replaced on the new agent by the possibility to use callbacks in the form definition.
Here is an example of a load hook where the default value of a field is set to 50 euros converted into dollars:
from django_forest.utils.collection import CollectionclassCustomersForest(Collection):defload(self): self.actions = [{"type":"single","name":"Charge credit card","fields": [{"field":"amount","type":"number",}, ],"hooks":{"load": self.send_invoice_load_hook,},}, ]defsend_invoice_load_hook(self,fields,request,*args,**kwargs): amount_field =next((x for x in fields if x["field"] =="amount"), None) amount_field["value"]=convertEurosIntoDollars(50)return fields
from forestadmin.datasource_toolkit.decorators.action.context.single import ActionContextSingledefamount_default_value(context: ActionContextSingle):returnconvertEurosIntoDollars(50)agent.customize_collection('customers').add_action('Charge credit card', {"scope": "Single","form": [ {"field": 'amount',"type": "Number",# the function given can also be an async function,# it will be automatically awaited"default_value": amount_default_value,# or a lambda# "default_value": lambda context: convertEurosIntoDollars(50), }, ]})
And another for a change hook which makes a field required if the value of another field is greater than 100:
from django_forest.utils.collection import CollectionclassCustomersForest(Collection):defload(self): self.actions = [{"type":"single","name":"Charge credit card","fields": [{"field":"amount","type":"number","hook":"on_amount_change",},{"field":"motivation","type":"string","is_required":False,}, ],"hooks":{"change":{"on_amount_change": on_amount_change,},},}, ]defon_amount_change(self,fields,request,changed_field,*args,**kwargs): amount_field =next((x for x in fields if x["field"] =="amount"), None) motivation_field =next( (x for x in fields if x["field"] =="motivation"), None ) motivationField["is_required"]= amount_field["value"]>100return fields
from forestadmin.datasource_toolkit.decorators.action.context.single import ActionContextSingleasyncdefamount_is_required(context: ActionContextSingle):return context.form_values.get("amount", 0)>100agent.customize_collection('customers').add_action('Charge credit card', {"scope": "Single","form": [ {"field": 'amount',"type": "Number", }, {"field": 'motivation',"type": "String",# the function given can also be an async function,# it will be automatically awaited"is_required": amount_is_required,# or a lambda# "is_required": lambda context:context.form_values.get("amount", 0)>100, }, ]})