* feat: global `frappe.in_test` flag * feat: helper utility to toggle `frappe.in_test` * fix: use `toggle_test_mode` util * fix: use `frappe.in_test` * chore: add comment explaining global `in_test` * chore: ignore commit replacing flag usage * test: temporarily disable `frappe.in_test` this worked earlier because flag was set in werkzeug.local which was separate for API test client * test: add comment explaining change
172 lines
5.2 KiB
Python
172 lines
5.2 KiB
Python
"""
|
|
Traced Fields for Frappe
|
|
|
|
This module provides utilities for creating traced fields in Frappe documents,
|
|
which is particularly useful for enforcing strict value lifetime validation rules.
|
|
|
|
Key features:
|
|
- Create fields that can be monitored for specific value changes
|
|
- Enforce forbidden values on fields
|
|
- Apply custom validation logic to fields
|
|
- Seamlessly integrate with Frappe's document model
|
|
|
|
Example of standard usage:
|
|
from frappe.model.trace import TracedDocument, traced_field
|
|
|
|
class CustomSalesInvoice(SalesInvoice, TracedDocument):
|
|
...
|
|
def validate_amount(self, value):
|
|
if value < 0:
|
|
raise AssertionError("Amount cannot be negative")
|
|
|
|
loyalty_program = traced_field("Loyalty Program", forbidden_values = ["FORBIDDEN_PROGRAM"])
|
|
amount = traced_field("Amount", custom_validation = validate_amount)
|
|
...
|
|
|
|
See frappe.tests.classes.context_managers for a context manager built into test classes.
|
|
"""
|
|
|
|
import frappe
|
|
from frappe.model.document import Document
|
|
|
|
|
|
class TracedValue:
|
|
"""
|
|
A descriptor class for creating traced fields in Frappe documents.
|
|
|
|
This class allows for monitoring and validating changes to specific fields
|
|
in a Frappe document. It can enforce forbidden values and apply custom
|
|
validation logic.
|
|
|
|
Attributes:
|
|
field_name (str): The name of the field being traced.
|
|
forbidden_values (list): A list of values that are not allowed for this field.
|
|
custom_validation (callable): A function for custom validation logic.
|
|
"""
|
|
|
|
def __init__(self, field_name, forbidden_values=None, custom_validation=None):
|
|
"""
|
|
Initialize a TracedValue instance.
|
|
|
|
Args:
|
|
field_name (str): The name of the field to be traced.
|
|
forbidden_values (list, optional): A list of values that should not be allowed.
|
|
custom_validation (callable, optional): A function for additional validation.
|
|
"""
|
|
self.field_name = field_name
|
|
self.forbidden_values = forbidden_values or []
|
|
self.custom_validation = custom_validation
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
"""
|
|
Get the value of the traced field.
|
|
|
|
Args:
|
|
obj (object): The instance that this descriptor is accessed from.
|
|
objtype (type, optional): The type of the instance.
|
|
|
|
Returns:
|
|
The value of the traced field, or self if accessed from the class.
|
|
"""
|
|
if obj is None:
|
|
return self
|
|
|
|
return getattr(obj, f"_{self.field_name}", None)
|
|
|
|
def __set__(self, obj, value):
|
|
"""
|
|
Set the value of the traced field with validation.
|
|
|
|
This method checks against forbidden values and applies custom validation
|
|
before setting the value.
|
|
|
|
Args:
|
|
obj (object): The instance that this descriptor is accessed from.
|
|
value: The value to set for the traced field.
|
|
|
|
Raises:
|
|
ValueError: If the value is forbidden or fails custom validation.
|
|
Note: returns AssertionError in test mode to debug with the `--pdb` flag.
|
|
|
|
"""
|
|
if value in self.forbidden_values:
|
|
if frappe.in_test:
|
|
frappe.throw(f"{self.field_name} cannot be set to {value}", AssertionError)
|
|
else:
|
|
frappe.throw(f"{self.field_name} cannot be set to {value}")
|
|
|
|
if self.custom_validation:
|
|
try:
|
|
self.custom_validation(obj, value)
|
|
except Exception as e:
|
|
if frappe.in_test:
|
|
frappe.throw(str(e), AssertionError)
|
|
else:
|
|
frappe.throw(str(e))
|
|
|
|
setattr(obj, f"_{self.field_name}", value)
|
|
|
|
|
|
class TracedDocument(Document):
|
|
"""
|
|
A base class for Frappe documents with traced fields.
|
|
|
|
This class extends Frappe's Document class to provide support for
|
|
traced fields created with TracedValue.
|
|
|
|
Attributes:
|
|
Inherits all attributes from frappe.model.document.Document
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""
|
|
Initialize a TracedDocument instance.
|
|
|
|
This method sets up traced fields and initializes the parent Document.
|
|
|
|
Args:
|
|
*args: Positional arguments to pass to the parent constructor.
|
|
**kwargs: Keyword arguments to pass to the parent constructor.
|
|
"""
|
|
super().__init__(*args, **kwargs)
|
|
for name, attr in self.__class__.__dict__.items():
|
|
if isinstance(attr, TracedValue):
|
|
setattr(self, f"_{name}", getattr(self, name))
|
|
|
|
def get_valid_dict(self, *args, **kwargs):
|
|
"""
|
|
Get a valid dictionary representation of the document.
|
|
|
|
This method extends the parent method to properly handle traced fields.
|
|
|
|
Args:
|
|
*args: Positional arguments to pass to the parent method.
|
|
**kwargs: Keyword arguments to pass to the parent method.
|
|
|
|
Returns:
|
|
dict: A dictionary representation of the document, including traced fields.
|
|
"""
|
|
d = super().get_valid_dict(*args, **kwargs)
|
|
for name, attr in self.__class__.__dict__.items():
|
|
if isinstance(attr, TracedValue):
|
|
d[name] = getattr(self, name)
|
|
return d
|
|
|
|
|
|
def traced_field(*args, **kwargs):
|
|
"""
|
|
A convenience function for creating TracedValue instances.
|
|
|
|
This function simplifies the creation of traced fields in Frappe documents.
|
|
|
|
Args:
|
|
*args: Positional arguments to pass to TracedValue constructor.
|
|
**kwargs: Keyword arguments to pass to TracedValue constructor.
|
|
|
|
Returns:
|
|
TracedValue: An instance of the TracedValue descriptor.
|
|
"""
|
|
return TracedValue(*args, **kwargs)
|
|
|
|
|
|
from frappe.deprecation_dumpster import model_trace_traced_field_context as traced_field_context
|