Skip to content

Best Practices

Guidelines for building maintainable, performant Fullfinity applications.

my_module/
├── __init__.py # Package marker (optional but recommended)
├── manifest.yaml # Module metadata
├── models/
│ ├── __init__.py
│ ├── main_model.py # One model per file
│ └── related_model.py
├── views/
│ ├── main_views.yaml # Views per model
│ ├── menus.yaml # Menu items
│ └── actions.yaml # Window actions
├── security/
│ ├── groups.yaml # Security groups
│ ├── access.yaml # Model access rules
│ └── rules.yaml # Record rules
└── data/
└── initial_data.yaml # 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):
for record in self:
await record.fetch_related("lines")
record.total = sum(line.subtotal for line in (record.lines or []))
# Bad: Manually updated
total = Monetary() # Must remember to update
# Good: Normalized relationship
customer = ManyToOne("Contact", related_name="orders", on_delete="SET NULL")
# 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
- type: field
name: state
properties:
widget: Badge
colors: {...}
# Boolean toggles: Switch (not `active` — archiving is the three-dot menu, never a form toggle)
- type: field
name: is_default
properties:
widget: Switch
# Currency: Monetary (renders the currency symbol; needs a currency_field)
- type: field
name: amount
properties:
widget: Monetary
currency_field: currency
# Good: Start restrictive
- data_type: ModelAccess
model: SaleOrder
group: sales_user_group
read_perm: true
create_perm: true
write_perm: true
delete_perm: false # Users can't delete
# Managers get delete
- data_type: ModelAccess
model: SaleOrder
group: sales_manager_group
read_perm: true
create_perm: true
write_perm: true
delete_perm: true
# Good: Users see only their records
- data_type: RecordRule
identifier: sale_order_own_rule
name: 'Sale Order: Own'
model: SaleOrder
groups:
- - R
- - sales_user_group
rule: Q(salesperson__id=uid)
read_perm: true
write_perm: true
create_perm: true
delete_perm: true
# Managers see all
- data_type: RecordRule
identifier: sale_order_all_rule
name: 'Sale Order: All'
model: SaleOrder
groups:
- - R
- - sales_manager_group
rule: Q(id__gt=0)
read_perm: true
write_perm: true
create_perm: true
delete_perm: true
# Good: Hide manager-only buttons
- type: actionButton
anchor: action_override_price
properties:
label: Override Price
method: override_price
groups: [sales_manager_group]

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 = [
{
"id": "confirmed",
"condition": "Q(state__in=['Confirmed', 'Done'])",
"exclusions": ["note", "internal_note"],
"message": "This order is confirmed. Cancel it to change the lines.",
},
]

This ensures:

  • Fields are locked when the record reaches certain states
  • Enforced at both backend (API) and frontend (UI) from the same declaration
  • Only the rule’s exclusions remain editable
  • Cannot be bypassed by direct API calls
  • Another module can add its own rule, or override this one by its id, without either of you restating the other’s condition — see Controlled Edits
# 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 = get_model("User")
other = await User.create(name="Other", email="other@test.fullfinity")
# act_as_role creates/reuses a single user holding the given role; grab it so
# we can make an order it owns. Fixtures are staged elevated (outside the block).
async with self.act_as_role("sales_user_group"):
me = type(self).env.user
own_order = await SaleOrder.create(salesperson=me.id)
other_order = await SaleOrder.create(salesperson=other.id)
# Re-enter as that same role user with ACL enforced, so the record rule applies.
async with self.act_as_role("sales_user_group"):
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
- type: field
name: priority
properties:
widget: Badge
colors:
urgent: red
normal: blue
  1. Organize - Clear module structure, consistent naming
  2. Design - Focused models, calculated fields, relationships
  3. Secure - Least privilege, record rules, group-based UI
  4. Optimize - Avoid N+1, use limits, index wisely
  5. Test - Security, calculated fields, workflows
  6. Document - Model purpose, complex fields, workflows