Create and manage Smart Actions

What is a Smart Action?

Sooner or later, you will need to perform actions on your data that are specific to your business. Moderating comments, generating an invoice, logging into a customer’s account or banning a user are exactly the kind of important tasks to unlock in order to manage your day-to-day operations.
On our Live Demo example, our companies collection has many examples of Smart Action. The simplest one is Mark as live.
If you're looking for information on native actions (CRUD), check out this page.

Creating a Smart action

In order to create a Smart action, you will first need to declare it in your code for a specific collection. Here we declare a Mark as Live Smart action for the companies collection.
SQL
Mongodb
Rails
Django
/forest/companies.js
1
const { collection } = require('forest-express-sequelize');
2
3
collection('companies', {
4
actions: [{
5
name: 'Mark as Live'
6
}],
7
});
Copied!
/forest/companies.js
1
const { collection } = require('forest-express-mongoose');
2
3
collection('companies', {
4
actions: [{
5
name: 'Mark as Live'
6
}],
7
});
Copied!
/lib/forest_liana/collections/company.rb
1
class Forest::Company
2
include ForestLiana::Collection
3
4
collection :Company
5
6
action 'Mark as Live'
7
end
Copied!
app/forest/companies.py
1
from django_forest.utils.collection import Collection
2
from app.models import Company
3
4
class CompanyForest(Collection):
5
def load(self):
6
self.actions = [{
7
'name': 'Mark as Live'
8
}]
9
10
Collection.register(CompanyForest, Company)
Copied!
After declaring it, your Smart action will appear in the Smart actions tab within your collection settings.
A Smart action is displayed in the UI only if:
  • it is set as "visible" (see screenshot below) AND
  • in non-development environments, the user's role must grant the "trigger" permission
You must make the action visible there if you wish users to be able to see it.
It will then show in the actions dropdown button:
At this point, the Smart Action does nothing, because no route in your Admin backend handles the API call yet.
The Smart Action behavior is implemented separately from the declaration.
In the following example, we've implemented the Mark as live Smart Action, which simply changes a company's status to live.
SQL
Mongodb
Rails
Django
/routes/companies.js
1
...
2
3
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
4
const recordsGetter = new RecordsGetter(companies, request.user, request.query);
5
6
return recordsGetter.getIdsFromRequest(req)
7
.then(companyIds => companies.update({ status: 'live' }, { where: { id: companyIds }}))
8
.then(() => res.send({ success: 'Company is now live!' }));
9
});
10
11
...
12
13
module.exports = router;
Copied!
/routes/companies.js
1
...
2
3
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
4
const recordsGetter = new RecordsGetter(companies, request.user, request.query);
5
6
return recordsGetter.getIdsFromRequest(req)
7
.then(companyIds => companies.updateMany({ _id: { $in: companyIds }}, { $set: { status: 'live' }}))
8
.then(() => res.send({ success: 'Company is now live!' }));
9
});
10
11
...
12
13
module.exports = router;
Copied!
The route declaration takes place in config/routes.rb.
/config/routes.rb
1
Rails.application.routes.draw do
2
# MUST be declared before the mount ForestLiana::Engine.
3
namespace :forest do
4
post '/actions/mark-as-live' => 'companies#mark_as_live'
5
end
6
7
mount ForestLiana::Engine => '/forest'
8
end
Copied!
The business logic in this Smart Action is extremely simple. We only update here the attribute status of the companies to the value live:
/app/controllers/forest/companies_controller.rb
1
class Forest::CompaniesController < ForestLiana::SmartActionsController
2
def mark_as_live
3
company_id = ForestLiana::ResourcesGetter.get_ids_from_request(params).first
4
Company.update(company_id, status: 'live')
5
6
head :no_content
7
end
8
end
Copied!
Note that Forest Admin takes care of the authentication thanks to the ForestLiana::SmartActionsController parent class controller.
You may have to add CORS headers to enable the domain app.forestadmin.com to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:3000).
Make sure your project urls.py file include you app urls with the forest prefix.
urls.py
1
from django.contrib import admin
2
from django.urls import path, include
3
4
urlpatterns = [
5
path('forest', include('app.urls')),
6
path('forest', include('django_forest.urls')),
7
path('admin/', admin.site.urls),
8
]
Copied!
The route declaration takes place in app/urls.py.
app/urls.py
1
from django.urls import path
2
from django.views.decorators.csrf import csrf_exempt
3
4
from . import views
5
6
app_name = 'app'
7
urlpatterns = [
8
path('/actions/mark-as-live', csrf_exempt(views.MarkAsLiveView.as_view()), name='mark-as-live'),
9
]
Copied!
The business logic in this Smart Action is extremely simple. We only update here the attribute status of the companies to the value live:
app/views.py
1
from django.http import JsonResponse
2
from django_forest.utils.views.action import ActionView
3
4
5
class MarkAsLiveView(ActionView):
6
7
def post(self, request, *args, **kwargs):
8
params = request.GET.dict()
9
body = self.get_body(request.body)
10
ids = self.get_ids_from_request(request, self.Model)
11
12
return JsonResponse({'success': 'live!'})
Copied!
Note that Forest Admin takes care of the authentication thanks to the ActionView parent class view.
You may have to add CORS headers to enable the domain app.forestadmin.com to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:8000).

What's happening under the hood?

When you trigger the Smart Action from the UI, your browser will make an API call: POST /forest/actions/mask-as-live.
If you want to customize the API call, check the list of available options.
The payload of the HTTP request is based on a JSON API document. The data.attributes.ids key allows you to retrieve easily the selected records from the UI. The data.attributes.values key contains all the values of your input fields (handling input values). Other properties of data.attributes are used to manage the select all behavior.
payload example
1
{
2
"data": {
3
"attributes": {
4
"ids": ["1985"],
5
"values": {},
6
"collection_name": "companies",
7
...
8
},
9
"type": "custom-action-requests"
10
}
11
}
Copied!
Should you want not to use the RecordsGetter and use request attributes directly instead, be very careful about edge cases (related data view, etc).

Available Smart Action options

Here is the list of available options to customize your Smart Action:
Name
Type
Description
name
string
Label of the action displayed in Forest Admin.
type
string
(optional) Type of the action. Can be bulk, global or single. Default is bulk.
fields
array of objects
(optional) Check the handling input values section.
download
boolean
(optional) If true, the action triggers a file download in the Browser. Default is false
endpoint
string
(optional) Set the API route to call when clicking on the Smart Action. Default is '/forest/actions/name-of-the-action-dasherized'
httpMethod
string
(optional) Set the HTTP method to use when clicking on the Smart Action. Default is POST.
Want to go further with Smart Actions? Read the next page to discover how to make your Smart Actions even more powerful with Forms!

Available Smart Action properties

req.user

The JWT Data Token contains all the details of the requesting user. On any authenticated request to your Admin Backend, you can access them with the variable req.user.
1
req.user content example
2
3
{
4
"id": "172",
5
"email": "[email protected]",
6
"firstName": "Angelica",
7
"lastName": "Bengtsson",
8
"team": "Pole Vault",
9
"role": "Manager",
10
"tags": [{ key: "country", value: "Canada" }],
11
"renderingId": "4998",
12
"iat": 1569913709,
13
"exp": 1571123309
14
}
Copied!

req.body

You can find important information in the body of the request.
This is particularly useful to find the context in which an action was performed via a relationship.
1
{
2
data: {
3
attributes: {
4
collection_name: 'users', //collection on which the action has been triggered
5
values: {},
6
ids: [Array], //IDs of selected records
7
parent_collection_name: 'companies', //Parent collection name
8
parent_collection_id: '1', //Parent collection id
9
parent_association_name: 'users', //Name of the association
10
all_records: false,
11
all_records_subset_query: {},
12
all_records_ids_excluded: [],
13
smart_action_id: 'users-reset-password'
14
},
15
type: 'custom-action-requests'
16
}
17
}
Copied!

Customizing response

Default success notification

Returning a 204 status code to the HTTP request of the Smart Action shows the default notification message in the browser.
On our Live Demo example, if our Smart Action Mark as Live route is implemented like this:
/routes/companies.js
1
...
2
3
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
4
// ...
5
res.status(204).send();
6
});
7
8
...
Copied!
We will see a success message in the browser:

Custom success notification

If we return a 200 status code with an object { success: '...' } as the payload like this…
SQL
Mongodb
Rails
Django
/routes/companies.js
1
...
2
3
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
4
// ...
5
res.send({ success: 'Company is now live!' });
6
});
7
8
...
Copied!
/routes/companies.js
1
...
2
3
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
4
// ...
5
res.send({ success: 'Company is now live!' });
6
});
7
8
...
Copied!
1
class Forest::CompaniesController < ForestLiana::SmartActionsController
2
def mark_as_live
3
# ...
4
render json: { success: 'Company is now live!' }
5
end
6
end
Copied!
app/views.py
1
from django.http import JsonResponse
2
from django_forest.utils.views.action import ActionView
3
4
5
class MarkAsLiveView(ActionView):
6
7
def post(self, request, *args, **kwargs):
8
return JsonResponse({'success': 'Company is now live!'})
Copied!
… the success notification will look like this:

Custom error notification

Finally, returning a 400 status code allows you to return errors properly.
SQL
Mongodb
Rails
Django
/routes/companies.js
1
...
2
3
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
4
// ...
5
res.status(400).send({ error: 'The company was already live!' });
6
});
7
8
...
Copied!
/routes/companies.js
1
...
2
3
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
4
// ...
5
res.status(400).send({ error: 'The company was already live!' });
6
});
7
8
...
Copied!
/app/controllers/forest/companies_controller.rb
1
class Forest::CompaniesController < ForestLiana::SmartActionsController
2
def mark_as_live
3
# ...
4
render status: 400, json: { error: 'The company was already live!' }
5
end
6
end
Copied!
app/views.py
1
from django.http import JsonResponse
2
from django_forest.utils.views.action import ActionView
3
4
5
class MarkAsLiveView(ActionView):
6
7
def post(self, request, *args, **kwargs):
8
return JsonResponse({'error': 'The company was already live!'}, status=400)
9
Copied!

Custom HTML response

You can also return a HTML page as a response to give more feedback to the admin user who has triggered your Smart Action. To do this, you just need to return a 200 status code with an object { html: '...' }.
On our Live Demo example, we’ve created a Charge credit card Smart Action on the Collection customersthat returns a custom HTML response.
SQL
Mongodb
Rails
Django
/forest/companies.js
1
const { collection } = require('forest-express-sequelize');
2
3
collection('customers', {
4
actions: [{
5
name: 'Charge credit card',
6
type: 'single',
7
fields: [{
8
field: 'amount',
9
isRequired: true,
10
description: 'The amount (USD) to charge the credit card. Example: 42.50',
11
type: 'Number'
12
}, {
13
field: 'description',
14
isRequired: true,
15
description: 'Explain the reason why you want to charge manually the customer here',
16
type: 'String'
17
}]
18
}]
19
});
Copied!
/routes/customers.js
1
...
2
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4
router.post('/actions/charge-credit-card', permissionMiddlewareCreator.smartAction(), (req, res) => {
5
let customerId = req.body.data.attributes.ids[0];
6
let amount = req.body.data.attributes.values.amount * 100;
7
let description = req.body.data.attributes.values.description;
8
9
return customers
10
.findByPk(customerId)
11
.then((customer) => {
12
return stripe.charges.create({
13
amount: amount,
14
currency: 'usd',
15
customer: customer.stripe_id,
16
description: description
17
});
18
})
19
.then((response) => {
20
res.send({
21
html: `
22
<p class="c-clr-1-4 l-mt l-mb">\$${response.amount / 100} USD has been successfuly charged.</p>
23
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
24
<p class="c-clr-1-4 l-mb">**** **** **** ${response.source.last4}</p>
25
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
26
<p class="c-clr-1-4 l-mb">${response.source.exp_month}/${response.source.exp_year}</p>
27
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
28
<p class="c-clr-1-4 l-mb">${response.source.brand}</p>
29
<strong class="c-form__label--read c-clr-1-2">Country</strong>
30
<p class="c-clr-1-4 l-mb">${response.source.country}</p>
31
`
32
});
33
});
34
});
35
36
...
37
38
module.exports = router;
Copied!
/forest/companies.js
1
const { collection } = require('forest-express-mongoose');
2
3
collection('Customer', {
4
actions: [{
5
name: 'Charge credit card',
6
type: 'single',
7
fields: [{
8
field: 'amount',
9
isRequired: true,
10
description: 'The amount (USD) to charge the credit card. Example: 42.50',
11
type: 'Number'
12
}, {
13
field: 'description',
14
isRequired: true,
15
description: 'Explain the reason why you want to charge manually the customer here',
16
type: 'String'
17
}]
18
}]
19
});
Copied!
/routes/customers.js
1
...
2
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4
router.post('/actions/charge-credit-card', (req, res) => {
5
let customerId = req.body.data.attributes.ids[0];
6
let amount = req.body.data.attributes.values.amount * 100;
7
let description = req.body.data.attributes.values.description;
8
9
return Customer
10
.findById(customerId)
11
.then((customer) => {
12
return stripe.charges.create({
13
amount: amount,
14
currency: 'usd',
15
customer: customer.stripe_id,
16
description: description
17
});
18
})
19
.then((response) => {
20
res.send({
21
html: `
22
<p class="c-clr-1-4 l-mt l-mb">\$${response.amount / 100} USD has been successfuly charged.</p>
23
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
24
<p class="c-clr-1-4 l-mb">**** **** **** ${response.source.last4}</p>
25
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
26
<p class="c-clr-1-4 l-mb">${response.source.exp_month}/${response.source.exp_year}</p>
27
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
28
<p class="c-clr-1-4 l-mb">${response.source.brand}</p>
29
<strong class="c-form__label--read c-clr-1-2">Country</strong>
30
<p class="c-clr-1-4 l-mb">${response.source.country}</p>
31
`
32
});
33
});
34
});
35
36
...
37
38
module.exports = router;
Copied!
/lib/forest_liana/collections/customer.rb
1
class Forest::Customer
2
include ForestLiana::Collection
3
4
collection :Customer
5
6
action 'Charge credit card', type: 'single', fields: [{
7
field: 'amount',
8
is_required: true,
9
description: 'The amount (USD) to charge the credit card. Example: 42.50',
10
type: 'Number'
11
}, {
12
field: 'description',
13
is_required: true,
14
description: 'Explain the reason why you want to charge manually the customer here',
15
type: 'String'
16
}]
17
end
Copied!
/config/routes.rb
1
Rails.application.routes.draw do
2
# MUST be declared before the mount ForestLiana::Engine.
3
namespace :forest do
4
post '/actions/charge-credit-card' => 'customers#charge_credit_card'
5
end
6
7
mount ForestLiana::Engine => '/forest'
8
end
Copied!
/app/controllers/forest/customers_controller.rb
1
class Forest::CustomersController < ForestLiana::SmartActionsController
2
def charge_credit_card
3
customer_id = ForestLiana::ResourcesGetter.get_ids_from_request(params).first
4
amount = params.dig('data', 'attributes', 'values', 'amount').to_i
5
description = params.dig('data', 'attributes', 'values', 'description')
6
7
customer = Customer.find(customer_id)
8
9
response = Stripe::Charge.create(
10
amount: amount * 100,
11
currency: 'usd',
12
customer: customer.stripe_id,
13
description: description
14
)
15
16
render json: { html: <<EOF
17
<p class="c-clr-1-4 l-mt l-mb">$#{response.amount / 100.0} USD has been successfuly charged.</p>
18
19
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
20
<p class="c-clr-1-4 l-mb">**** **** **** #{response.source.last4}</p>
21
22
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
23
<p class="c-clr-1-4 l-mb">#{response.source.exp_month}/#{response.source.exp_year}</p>
24
25
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
26
<p class="c-clr-1-4 l-mb">#{response.source.brand}</p>
27
28
<strong class="c-form__label--read c-clr-1-2">Country</strong>
29
<p class="c-clr-1-4 l-mb">#{response.source.country}</p>
30
EOF
31
}
32
end
33
end
Copied!
app/forest/customer.py
1
from django_forest.utils.collection import Collection
2
from app.models import Company
3
4
class CompanyForest(Collection):
5
def load(self):
6
self.actions = [{
7
'name': 'Charge credit card',
8
'fields': [
9
{
10
'field': 'amount',
11
'description': 'The amount (USD) to charge the credit card. Example: 42.50',
12
'isRequired': True,
13
'type': 'Number'
14
},
15
{
16
'field': 'description',
17
'description': 'Explain the reason why you want to charge manually the customer here',
18
'isRequired': True,
19
'type': 'String'
20
},
21
],
22
}]
23
24
Collection.register(CompanyForest, Company)
Copied!
app/urls.py
1
from django.urls import path
2
from django.views.decorators.csrf import csrf_exempt
3
4
from . import views
5
6
app_name = 'app'
7
urlpatterns = [
8
path('/actions/charge-credit-card', csrf_exempt(views.ChargeCreditCardView.as_view()), name='charge-credit-card'),
9
]
Copied!
app/views.py
1
import stripe
2
3
from django.http import JsonResponse
4
from django_forest.utils.views.action import ActionView
5
6
from .models import Customer
7
8
class ChargeCreditCardView(ActionView):
9
10
def post(self, request, *args, **kwargs):
11
params = request.GET.dict()
12
body = self.get_body(request.body)
13
ids = self.get_ids_from_request(request, self.Model)
14
15
amount = body['data']['attributes']['values']['amount'] * 100
16
description = body['data']['attributes']['values']['description']
17
18
customer = Customer.object.get(pk=ids[0])
19
20
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
21
22
response = stripe.Charge.create(
23
amount=amount,
24
currency='usd',
25
customer=customer.stripe_id,
26
description=description,
27
)
28
29
data = f'''
30
<p class="c-clr-1-4 l-mt l-mb">${response['amount'] / 100} USD has been successfuly charged.</p>
31
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
32
<p class="c-clr-1-4 l-mb">**** **** **** {response['source']['last4']}</p>
33
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
34
<p class="c-clr-1-4 l-mb">{response['source']['exp_month']}/{response['source']['exp_year']}</p>
35
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
36
<p class="c-clr-1-4 l-mb">{response['source']['brand']}</p>
37
<strong class="c-form__label--read c-clr-1-2">Country</strong>
38
<p class="c-clr-1-4 l-mb">{response['source']['country']}</p>
39
'''
40
41
return JsonResponse({'html': data}, safe=False)
Copied!

Setting up a webhook

After a smart action you can set up a HTTP (or HTTPS) callback - a webhook - to forward information to other applications. To set up a webhook all you have to do is to add a webhookobject in the response of your action.
SQL
Mongodb
Rails
Django
1
response.send({
2
webhook: { // This is the object that will be used to fire http calls.
3
url: 'http://my-company-name', // The url of the company providing the service.
4
method: 'POST', // The method you would like to use (typically a POST).
5
headers: { }, // You can add some headers if needed (you can remove it).
6
body: { // A body to send to the url (only JSON supported).
7
adminToken: 'your-admin-token',
8
},
9
},
10
});
Copied!
1
response.send({
2
webhook: { // This is the object that will be used to fire http calls.
3
url: 'http://my-company-name', // The url of the company providing the service.
4
method: 'POST', // The method you would like to use (typically a POST).
5
headers: { }, // You can add some headers if needed (you can remove it).
6
body: { // A body to send to the url (only JSON supported).
7
adminToken: 'your-admin-token',
8
},
9
},
10
});
Copied!
1
render json: {
2
webhook: { # This is the object that will be used to fire http calls.
3
url: 'http://my-company-name', # The url of the company providing the service.
4
method: 'POST', # The method you would like to use (typically a POST).
5
headers: {}, # You can add some headers if needed (you can remove it).
6
body: { # A body to send to the url (only JSON supported).
7
adminToken: 'your-admin-token',
8
}
9
}
10
}
Copied!
1
return JsonResponse({
2
'webhook': { # This is the object that will be used to fire http calls.
3
'url': 'http://my-company-name', # The url of the company providing the service.
4
'method': 'POST', # The method you would like to use (typically a POST).
5
'headers': {}, # You can add some headers if needed (you can remove it).
6
'body': { # A body to send to the url (only JSON supported).
7
'adminToken': 'your-admin-token',
8
}
9
}
10
})
Copied!
Webhooks are commonly used to perform smaller requests and tasks, like sending emails or impersonating a user.
Another interesting use of this is automating SSO authentication into your external apps.

Downloading a file

SQL
Mongodb
Rails
Django
On our Live Demo, the collection customers has a Smart Action Generate invoice. In this use case, we want to download the generated PDF invoice after clicking on the action. To indicate a Smart Action returns something to download, you have to enable the option download.
/forest/customers.js
1
const { collection } = require('forest-express-sequelize');
2
3
collection('customers', {
4
actions: [{
5
name: 'Generate invoice',
6
download: true // If true, the action triggers a file download in the Browser.
7
}]
8
});
Copied!
/routes/customers.js
1
...
2
3
router.post('/actions/generate-invoice', permissionMiddlewareCreator.smartAction(),
4
(req, res) => {
5
let options = {
6
root: __dirname + '/../public/',
7
dotfiles: 'deny',
8
headers: {
9
'Access-Control-Expose-Headers': 'Content-Disposition',
10
'Content-Disposition': 'attachment; filename="invoice-2342.pdf"'
11
}
12
};
13
14
let fileName = 'invoice-2342.pdf';
15
res.sendFile(fileName, options, (error) => {
16
if (error) { next(error); }
17
});
18
});
19
20
...
21
22
module.exports = router;
Copied!
On our Live Demo, the collection customers has a Smart Action Generate invoice. In this use case, we want to download the generated PDF invoice after clicking on the action. To indicate a Smart Action returns something to download, you have to enable the option download.
/forest/customers.js
1
const { collection } = require('forest-express-mongoose');
2
3
collection('Customer', {
4
actions: [{
5
name: 'Generate invoice',
6
download: true // If true, the action triggers a file download in the Browser.
7
}]
8
});
Copied!
/routes/customers.js
1
...
2
3
router.post('/actions/generate-invoice', Liana.ensureAuthenticated,
4
(req, res) => {
5
let options = {
6
root: __dirname + '/../public/',
7
dotfiles: 'deny',
8
headers: {
9
'Access-Control-Expose-Headers': 'Content-Disposition',
10
'Content-Disposition': 'attachment; filename="invoice-2342.pdf"'
11
}
12
};
13
14
let fileName = 'invoice-2342.pdf';
15
res.sendFile(fileName, options, (error) => {
16
if (error) { next(error); }
17
});
18
});
19
20
...
21
22
module.exports = router;
Copied!
On our Live Demo, the collection Customer has a Smart Action Generate invoice. In this use case, we want to download the generated PDF invoice after clicking on the action. To indicate a Smart Action returns something to download, you have to enable the option download.
Don’t forget to expose the Content-Disposition header in the CORS configuration (as shown in the code below) to be able to customize the filename to download.
/lib/forest_liana/collections/customer.rb
1
class Forest::Customer
2
include ForestLiana::Collection
3
4
collection :Customer
5
6
action 'Generate invoice', download: true
7
end
Copied!
/config/routes.rb
1
Rails.application.routes.draw do
2
# MUST be declared before the mount ForestLiana::Engine.
3
namespace :forest do
4
post '/actions/generate-invoice' => 'customers#generate_invoice'
5
end
6
7
mount ForestLiana::Engine => '/forest'
8
end
Copied!
/config/application.rb
1
module LiveDemoRails
2
class Application < Rails::Application
3
config.middleware.insert_before 0, Rack::Cors do
4
allow do
5
origins '*'
6
resource '*', :headers => :any, :methods => [:get, :post, :options],
7
# you MUST expose the Content-Disposition header to customize the file to download.
8
expose: ['Content-Disposition']
9
end
10
end
11
end
12
end
Copied!
/app/controllers/forest/customers_controller.rb
1
class Forest::CustomersController < ForestLiana::SmartActionsController
2
def generate_invoice
3
data = open("#{File.dirname(__FILE__)}/../../../public/invoice-2342.pdf" )
4
send_data data.read, filename: 'invoice-2342.pdf', type: 'application/pdf', disposition: 'attachment'
5
end
6
end
Copied!
On our Live Demo, the collection Customer has a Smart Action Generate invoice. In this use case, we want to download the generated PDF invoice after clicking on the action. To indicate a Smart Action returns something to download, you have to enable the option download.
Don’t forget to expose the Content-Disposition header in the CORS configuration (as shown in the code below) to be able to customize the filename to download.
app/forest/customer.py
1
from django_forest.utils.collection import Collection
2
from app.models import Customer
3
4
class CustomerForest(Collection):
5
def load(self):
6
self.actions = [{
7
'name': 'Generate invoice',
8
'download': True
9
}]
10
11
Collection.register(CustomerForest, Customer)
Copied!
app/urls.py
1
from django.urls import path
2
from django.views.decorators.csrf import csrf_exempt
3
4
from . import views
5
6
app_name = 'app'
7
urlpatterns = [
8
path('/actions/generate-invoice', csrf_exempt(views.GenerateInvoiceView.as_view()), name='generate-invoice'),
9
]
Copied!
app/views.py
1
from datetime import datetime
2
from django.http import HttpResponse
3
from django_forest.utils.views.action import ActionView
4
5
class GenerateInvoiceView(ActionView):
6
7
def post(self, request, *args, **kwargs):
8
params = request.GET.dict()
9
body = self.get_body(request.body)
10
ids = self.get_ids_from_request(request, self.Model)
11
12
with open('public/invoice-2342.pdf', rb) as f:
13
file_data = f.read()
14
return HttpResponse(
15
file_data,
16
content_type='application/pdf',
17
headers={
18
'Content-Disposition': f'attachment; filename="invoice-2342.pdf"',
19
'Last-Modified': datetime.now(),
20
'X-Accel-Buffering': 'no',
21
'Cache-Control': 'no-cache'
22
},
23
)
Copied!
Want to upload your files to Amazon S3? Check out this this Woodshop tutorial.
If you want to create an action accessible from the details or the summary view of a record involving related data, this section may interest you.
In the example below, the “Add new transaction” action (1) is accessible from the summary view. This action creates a new transaction and automatically refresh the “Emitted transactions” related data section (2) to see the new transaction.
SQL
Mongodb
Rails
Django
Below is the sample code. We use faker to generate random data in our example. Remember to install it if you wish to use it (