Skip to content

Best Practices

Guidelines for building maintainable, performant Fullfinity applications.

my_module/
├── __init__.py # Package marker (optional but recommended)
├── manifest.json # Module metadata
├── models/
│ ├── __init__.py
│ ├── main_model.py # One model per file
│ └── related_model.py
├── views/
│ ├── main_views.json # Views per model
│ ├── menus.json # Menu items
│ └── actions.json # Window actions
├── security/
│ ├── groups.json # Security groups
│ ├── access.json # Model access rules
│ └── rules.json # Record rules
└── data/
└── initial_data.json # Default data
ItemConventionExample
Module foldersnake_casesales_crm
Model classPascalCaseSaleOrder
Model _namePascalCase"SaleOrder"
Field namesnake_caseexpected_revenue
View identifiersnake_casesale_order_form_view
Menu identifiersnake_casesale_order_menu
# Good: Single responsibility
class SaleOrder(Model):
"""Manages sales orders."""
pass
class SaleOrderLine(Model):
"""Individual line items on orders."""
pass
# Bad: God model
class Sale(Model):
"""Everything sales-related."""
# 50+ fields, multiple concerns
pass
# Good: Computed from source data
class SaleOrder(Model):
lines = OneToMany(related_model="SaleOrderLine", related_name="order")
total = Monetary(calculate="compute_total", store=True)
@Model.calculate("lines", "lines.subtotal")
async def compute_total(self):
await self.fetch_related("lines")
self.total = sum(line.subtotal for line in (self.lines or []))
# Bad: Manually updated
total = Monetary() # Must remember to update
# Good: Normalized relationship
customer = ManyToOne("Contact", related_name="orders")
# Access via relationship
order = await SaleOrder.filter(id=order_id).first()
await order.fetch_related("customer")
print(order.customer.name)
# Bad: Duplicated data
customer_name = Char(max_length=255)
customer_email = Char(max_length=255)
customer_phone = Char(max_length=50)
# Good: Sensible defaults
active = Boolean(default=True)
state = Selection(choices=["Draft", "Confirmed", "Done"], default="Draft")
date = Date(default=lambda self: date.today())
# Good: Computed default using lambda (get_current_user is a helper from
# fullfinity.engine.base, not a Model method)
salesperson = ManyToOne(
"User",
related_name="orders",
on_delete="SET NULL",
default=lambda self: get_current_user(self)
)
// Good: Semantic targeting
{
"action": "add",
"target": {"field": "email"},
"position": "after",
"value": {...}
}
// Bad: Index-based (fragile)
{
"action": "add",
"target": "arch[1].content[0].content[5]",
"value": {...}
}
// Good: Provides stable extension points
{
"type": "row",
"anchor": "customer_info_section",
"content": [...]
}
// Extensions can target this:
{"target": {"anchor": "customer_info_section"}}
// Status fields: Badge
{"name": "state", "properties": {"widget": "Badge", "colors": {...}}}
// Boolean toggles: Switch
{"name": "active", "properties": {"widget": "Switch"}}
// Currency: NumberInput with prefix
{"name": "amount", "properties": {"widget": "NumberInput", "prefix": "$"}}
// Good: Start restrictive
{
"group": "sales.sales_user",
"read_perm": true,
"create_perm": true,
"write_perm": true,
"delete_perm": false // Users can't delete
}
// Managers get delete
{
"group": "sales.sales_manager",
"delete_perm": true
}
// Good: Users see only their records
{
"model": "SaleOrder",
"groups": ["sales.sales_user"],
"rule": "(Q(salesperson__id__eq=uid))"
}
// Managers see all
{
"model": "SaleOrder",
"groups": ["sales.sales_manager"],
"rule": "(Q(True))"
}
// Good: Hide manager-only buttons
{
"type": "actionButton",
"properties": {
"label": "Override Price",
"method": "override_price",
"groups": ["sales.sales_manager"]
}
}

Use Controlled Edits for State-Based Restrictions

Section titled “Use Controlled Edits for State-Based Restrictions”
# Good: Lock fields after confirmation
class SaleOrder(Model):
_controlled_edits = {
"exclusions": ["note", "internal_note"],
"condition": "Q(state__in=['confirmed', 'done', 'cancelled'])"
}

This ensures:

  • Fields are locked when record reaches certain states
  • Enforced at both backend (API) and frontend (UI)
  • Only specified exclusions remain editable
  • Cannot be bypassed by direct API calls
# Bad: N+1 queries
orders = await SaleOrder.filter().all()
for order in orders:
await order.fetch_related("customer") # Query per order
# Good: Prefetch related data
orders = await SaleOrder.filter().prefetch_related("customer").all()
for order in orders:
customer = order.customer # Already loaded
# Good: Paginate results
orders = await SaleOrder.filter(state="Draft").limit(50).offset(0).all()
# Bad: Load everything
all_orders = await SaleOrder.filter().all() # Could be millions
class SaleOrder(Model):
# Fields used in filters/sorting should be indexed
state = Selection(choices=["Draft", "Confirmed", "Done"], index=True)
date = Date(index=True)
customer = ManyToOne("Contact", related_name="orders", on_delete="SET NULL", index=True)
from fullfinity.engine.base import UserError, ValidationError
async def confirm_order(self):
if not self.lines:
raise UserError("Cannot confirm order without lines")
if self.total <= 0:
raise ValidationError("Order total must be positive")
class SaleOrder(Model):
@Model.validate("discount")
async def check_discount(self):
if self.discount > 50:
raise ValidationError("Discount cannot exceed 50%")
class TestOrderSecurity(TestCase):
async def test_user_sees_own_orders(self):
user = await self.create_test_user(groups=["sales.sales_user"])
# Create orders for different users
own_order = await SaleOrder.create(salesperson=user.id)
other_order = await SaleOrder.create(salesperson=other_user.id)
# Run the rest of the test as that user so record rules apply
self.act_as(user)
orders = await SaleOrder.filter().all()
self.assertEqual(len(orders), 1)
self.assertEqual(orders[0].id, own_order.id)
async def test_order_total():
order = await SaleOrder.create(name="SO-TEST")
await SaleOrderLine.create(order=order.id, subtotal=100)
await SaleOrderLine.create(order=order.id, subtotal=50)
# Reload from the database to pick up the recomputed stored total
reloaded = await SaleOrder.filter(id=order.id).first()
assert reloaded.total == 150
class SaleOrder(Model):
"""
Sales Order manages customer orders.
Workflow:
- Draft: Initial state, can be edited
- Confirmed: Locked for processing
- Done: Completed and delivered
- Cancelled: Order cancelled
Related Models:
- SaleOrderLine: Individual line items
- Contact: Customer information
- Product: Ordered products
"""
probability = Integer(
description="Probability (%)",
hint="Likelihood of closing this opportunity. "
"Updated automatically based on stage, "
"can be manually overridden."
)
from fullfinity.engine.models import elevate
# Bad: Always using elevate()
with elevate():
orders = await SaleOrder.filter().all()
# Good: Use elevate() only when necessary
with elevate():
public_info = await Product.filter(public=True).all()
# Bad: Hardcoded stage ID
await lead.update(stage=5)
# Good: Look the record up by its seed identifier
CrmStage = get_model("CrmStage")
won_stage = await CrmStage.filter(identifier="stage_won").first()
await lead.update(stage=won_stage.id)
# Bad: Model doing view logic
class SaleOrder(Model):
def get_kanban_color(self):
return "red" if self.urgent else "blue"
# Good: Keep in view definition
{
"properties": {
"widget": "Badge",
"colors": {"urgent": "red", "normal": "blue"}
}
}
  1. Organize - Clear module structure, consistent naming
  2. Design - Focused models, computed fields, relationships
  3. Secure - Least privilege, record rules, group-based UI
  4. Optimize - Avoid N+1, use limits, index wisely
  5. Test - Security, computed fields, workflows
  6. Document - Model purpose, complex fields, workflows