Best Practices
Guidelines for building maintainable, performant Fullfinity applications.
Module Organization
Section titled “Module Organization”File Structure
Section titled “File Structure”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 dataNaming Conventions
Section titled “Naming Conventions”| Item | Convention | Example |
|---|---|---|
| Module folder | snake_case | sales_crm |
| Model class | PascalCase | SaleOrder |
Model _name | PascalCase | "SaleOrder" |
| Field name | snake_case | expected_revenue |
| View identifier | snake_case | sale_order_form_view |
| Menu identifier | snake_case | sale_order_menu |
Model Design
Section titled “Model Design”Keep Models Focused
Section titled “Keep Models Focused”# Good: Single responsibilityclass SaleOrder(Model): """Manages sales orders.""" pass
class SaleOrderLine(Model): """Individual line items on orders.""" pass
# Bad: God modelclass Sale(Model): """Everything sales-related.""" # 50+ fields, multiple concerns passUse Computed Fields
Section titled “Use Computed Fields”# Good: Computed from source dataclass 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 updatedtotal = Monetary() # Must remember to updatePrefer ManyToOne Over Denormalization
Section titled “Prefer ManyToOne Over Denormalization”# Good: Normalized relationshipcustomer = ManyToOne("Contact", related_name="orders")
# Access via relationshiporder = await SaleOrder.filter(id=order_id).first()await order.fetch_related("customer")print(order.customer.name)
# Bad: Duplicated datacustomer_name = Char(max_length=255)customer_email = Char(max_length=255)customer_phone = Char(max_length=50)Default Values
Section titled “Default Values”# Good: Sensible defaultsactive = 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))View Design
Section titled “View Design”Use Semantic Targeting
Section titled “Use Semantic Targeting”// 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": {...}}Add Anchors for Extension Points
Section titled “Add Anchors for Extension Points”// Good: Provides stable extension points{ "type": "row", "anchor": "customer_info_section", "content": [...]}
// Extensions can target this:{"target": {"anchor": "customer_info_section"}}Consistent Widget Usage
Section titled “Consistent Widget Usage”// 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": "$"}}Security
Section titled “Security”Principle of Least Privilege
Section titled “Principle of Least Privilege”// 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}Always Define Record Rules
Section titled “Always Define Record Rules”// 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))"}Use Groups for UI Elements
Section titled “Use Groups for UI Elements”// 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 confirmationclass 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
Performance
Section titled “Performance”Avoid N+1 Queries
Section titled “Avoid N+1 Queries”# Bad: N+1 queriesorders = await SaleOrder.filter().all()for order in orders: await order.fetch_related("customer") # Query per order
# Good: Prefetch related dataorders = await SaleOrder.filter().prefetch_related("customer").all()for order in orders: customer = order.customer # Already loadedUse Search Limits
Section titled “Use Search Limits”# Good: Paginate resultsorders = await SaleOrder.filter(state="Draft").limit(50).offset(0).all()
# Bad: Load everythingall_orders = await SaleOrder.filter().all() # Could be millionsIndex Frequently Queried Fields
Section titled “Index Frequently Queried Fields”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)Error Handling
Section titled “Error Handling”User-Friendly Errors
Section titled “User-Friendly Errors”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")Validate Before Save
Section titled “Validate Before Save”class SaleOrder(Model): @Model.validate("discount") async def check_discount(self): if self.discount > 50: raise ValidationError("Discount cannot exceed 50%")Testing
Section titled “Testing”Test Security Rules
Section titled “Test Security Rules”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)Test Computed Fields
Section titled “Test Computed Fields”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 == 150Documentation
Section titled “Documentation”Document Model Purpose
Section titled “Document Model Purpose”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 """Document Complex Fields
Section titled “Document Complex Fields”probability = Integer( description="Probability (%)", hint="Likelihood of closing this opportunity. " "Updated automatically based on stage, " "can be manually overridden.")Common Mistakes
Section titled “Common Mistakes”Don’t Bypass Security
Section titled “Don’t Bypass Security”from fullfinity.engine.models import elevate
# Bad: Always using elevate()with elevate(): orders = await SaleOrder.filter().all()
# Good: Use elevate() only when necessarywith elevate(): public_info = await Product.filter(public=True).all()Don’t Hardcode IDs
Section titled “Don’t Hardcode IDs”# Bad: Hardcoded stage IDawait lead.update(stage=5)
# Good: Look the record up by its seed identifierCrmStage = get_model("CrmStage")won_stage = await CrmStage.filter(identifier="stage_won").first()await lead.update(stage=won_stage.id)Don’t Mix Concerns
Section titled “Don’t Mix Concerns”# Bad: Model doing view logicclass 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"} }}Summary
Section titled “Summary”- Organize - Clear module structure, consistent naming
- Design - Focused models, computed fields, relationships
- Secure - Least privilege, record rules, group-based UI
- Optimize - Avoid N+1, use limits, index wisely
- Test - Security, computed fields, workflows
- Document - Model purpose, complex fields, workflows