Skip to content

Commit

Permalink
Use a factory rather than class to add boundary methods to models
Browse files Browse the repository at this point in the history
  • Loading branch information
jacklinke committed May 26, 2024
1 parent 242a65b commit 57de699
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 101 deletions.
103 changes: 51 additions & 52 deletions src/django_segments/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,57 @@
logger = logging.getLogger(__name__)


def boundary_helper_factory(range_field_name):
"""Factory function to create set_lower_boundary and set_upper_boundary model methods for a given range field.
Args:
range_field_name (str): The name of the range field.
Returns:
tuple: A tuple containing the set_lower_boundary and set_upper_boundary methods.
"""

def _set_boundary(self, range_field_name, lower=None, upper=None):
"""Set the boundary of the range field."""

range_field = getattr(self.model, range_field_name, None)
validate_value_type(self, value=lower if lower is not None else upper)

if lower is not None:
range_field.lower = lower

if upper is not None:
range_field.upper = upper

setattr(self.model, range_field_name, range_field)

def validate_value_type(self, value):
"""Validate the type of the provided value against the field_type."""
if value is None:
return

if not self.model.field_type in POSTGRES_RANGE_FIELDS.keys():
raise ValueError(f"Unsupported field type: {self.model.field_type} not in {POSTGRES_RANGE_FIELDS.keys()=}")

for key, val in POSTGRES_RANGE_FIELDS.items():
if key in self.model.field_type and not isinstance(value, val):
raise ValueError(f"Value must be a {val}, not {type(value)}")
raise ValueError(f"Unsupported field type: {self.model.field_type}")

def set_lower_boundary(self, value):
"""Set the lower boundary of the specified range field."""
_set_boundary(self, range_field_name, lower=value)

def set_upper_boundary(self, value):
"""Set the upper boundary of the specified range field."""
_set_boundary(self, range_field_name, upper=value)

return (
set_lower_boundary,
set_upper_boundary,
)


class ConcreteModelValidationHelper: # pylint: disable=R0903
"""Helper class for validating that models are concrete."""

Expand Down Expand Up @@ -163,58 +214,6 @@ def get_validated_segment_range_field(self) -> models.Field:
return self.get_validated_range_field()


class BoundaryHelper:
"""Helper class used by AbstractSpan and AbstractSegment to set the boundaries of the range field."""

def __init__(
self,
model: type[models.Model],
range_field_name_attr: str,
range_field_attr: str,
) -> None:
"""Initialize the helper with the model and range attributes."""
self.model = model
self.range_field_name_attr = range_field_name_attr
self.range_field_attr = range_field_attr

self.range_field_name = getattr(self.model, self.range_field_name_attr, None)
self.range_field = getattr(self.model, self.range_field_attr, None)

def set_lower_boundary(self, value):
"""Set the lower boundary of the range field."""
return self._set_boundary(lower=value)

def set_upper_boundary(self, value):
"""Set the upper boundary of the range field."""
return self._set_boundary(upper=value)

def _set_boundary(self, lower=None, upper=None):
"""Set the boundary of the range field."""

# Ensure that the provided value is of the correct type
self.validate_value_type(lower)
self.validate_value_type(upper)

# Set the boundary
if lower is not None:
self.range_field.lower = lower

if upper is not None:
self.range_field.upper = upper

return self.range_field

def validate_value_type(self, value):
"""Validate the type of the provided value against the field_type."""
if not self.model.field_type in POSTGRES_RANGE_FIELDS.keys():
raise ValueError(f"Unsupported field type: {self.model.field_type} not in {POSTGRES_RANGE_FIELDS.keys()=}")

for key, val in POSTGRES_RANGE_FIELDS.items():
if key in self.model.field_type and not isinstance(value, val):
raise ValueError(f"Value must be a {val}, not {type(value)}")
raise ValueError(f"Unsupported field type: {self.model.field_type}")


class SegmentSpanValidationHelper:
"""Helper class for validating that models have valid foreign key to a model that inherits from AbstractSpan.
Expand Down
26 changes: 9 additions & 17 deletions src/django_segments/models/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,20 @@

from django_segments.app_settings import ON_DELETE_FOR_PREVIOUS
from django_segments.models.base import AbstractSegmentMetaclass
from django_segments.models.base import BoundaryHelper
from django_segments.models.base import boundary_helper_factory


logger = logging.getLogger(__name__)


class AbstractSegment(models.Model, metaclass=AbstractSegmentMetaclass):
"""Abstract class from which all segment models should inherit.
"""Abstract class from which all Segment models should inherit.
All concrete subclasses of AbstractSegment must define either `segment_range_field_name` (a string representing the
name of the range field) or `segment_range` (a range field instance). If both or neither are defined, an
IncorrectSegmentError will be raised.
All concrete subclasses of AbstractSegment must define either `segment_span_field_name` (a string representing the
name of the foreign key field) or `segment_span` (a foreign key field instance). If both or neither are defined, an
IncorrectSegmentError will be raised.
When the concrete subclass is created, the segment_range_field and segment_span_field attributes are set to the
When the concrete subclass is created, the `segment_range_field` and `segment_span_field` attributes are set to the
range field and foreign key field instances, respectively. These attributes can be used to access the range field
and foreign key field instances in the concrete subclass.
Expand All @@ -43,6 +39,8 @@ class MyOtherSegment(AbstractSegment):
segment_span_field_name = 'my_span'
"""

_set_lower_boundary, _set_upper_boundary = boundary_helper_factory("segment_range_field")

deleted_at = models.DateTimeField(
_("Deleted at"),
null=True,
Expand All @@ -66,18 +64,12 @@ class Meta: # pylint: disable=C0115 disable=R0903
]

def set_lower_boundary(self, value):
"""Set the lower boundary of the range field."""
boundary_helper = BoundaryHelper(
model=self, range_field_name_attr="segment_range_field_name", range_field_attr="segment_range"
)
boundary_helper.set_lower_boundary(value)
"""Set the lower boundary of the segment range field."""
self._set_lower_boundary(self, value)

def set_upper_boundary(self, value):
"""Set the upper boundary of the range field."""
boundary_helper = BoundaryHelper(
model=self, range_field_name_attr="segment_range_field_name", range_field_attr="segment_range"
)
boundary_helper.set_upper_boundary(value)
"""Set the upper boundary of the segment range field."""
self._set_upper_boundary(self, value)

@property
def previous(self):
Expand Down
45 changes: 13 additions & 32 deletions src/django_segments/models/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,21 @@
from django_segments.app_settings import SOFT_DELETE
from django_segments.app_settings import STICKY_BOUNDARIES
from django_segments.models.base import AbstractSegmentMetaclass
from django_segments.models.base import BoundaryHelper
from django_segments.models.base import boundary_helper_factory


logger = logging.getLogger(__name__)


class AbstractSpan(models.Model, metaclass=AbstractSegmentMetaclass):
"""Abstract class from which all span models should inherit.
"""Abstract class from which all Span models should inherit.
All concrete subclasses of AbstractSpan must define either `initial_range_field_name` (a string representing the
name of the range field) or `initial_range` (a range field instance). If both or neither are defined, an
IncorrectSpan will be raised.
All concrete subclasses of AbstractSpan must define either `current_range_field_name` (a string representing the
name of the range field) or `current_range` (a range field instance). If both or neither are defined, an
IncorrectSpan will be raised.
When the concrete subclass is created, the initial_range_field and current_range_field attributes set to the range
field instances. These attributes can be used to access the range field instances in the concrete subclass.
When the concrete subclass is created, the `initial_range_field` and `current_range_field` attributes set to the
range field instances. These attributes can be used to access the range field instances in the concrete subclass.
Example:
Expand All @@ -38,6 +34,9 @@ class MySpan(AbstractSpan):
current_range = DateTimeRangeField()
"""

_set_initial_lower_boundary, _set_initial_upper_boundary = boundary_helper_factory("initial_range_field")
_set_lower_boundary, _set_upper_boundary = boundary_helper_factory("current_range_field")

deleted_at = models.DateTimeField(
_("Deleted at"),
null=True,
Expand Down Expand Up @@ -77,38 +76,20 @@ def get_segment_class(self):
return self.segment_span.field

def set_initial_lower_boundary(self, value):
"""Set the lower boundary of the initial range field.
Only used when creating a new span.
"""
boundary_helper = BoundaryHelper(
model=self, range_field_name_attr="initial_range_field_name", range_field_attr="initial_range"
)
boundary_helper.set_lower_boundary(value)
"""Set the lower boundary of the initial range field."""
self._set_initial_lower_boundary(self, value)

def set_initial_upper_boundary(self, value):
"""Set the upper boundary of the initial range field.
Only used when creating a new span.
"""
boundary_helper = BoundaryHelper(
model=self, range_field_name_attr="initial_range_field_name", range_field_attr="initial_range"
)
boundary_helper.set_upper_boundary(value)
"""Set the upper boundary of the initial range field."""
self._set_initial_upper_boundary(self, value)

def set_lower_boundary(self, value):
"""Set the lower boundary of the current range field."""
boundary_helper = BoundaryHelper(
model=self, range_field_name_attr="current_range_field_name", range_field_attr="current_range"
)
boundary_helper.set_lower_boundary(value)
self._set_lower_boundary(self, value)

def set_upper_boundary(self, value):
"""Set the upper boundary of the current range field."""
boundary_helper = BoundaryHelper(
model=self, range_field_name_attr="current_range_field_name", range_field_attr="current_range"
)
boundary_helper.set_upper_boundary(value)
self._set_upper_boundary(self, value)

def get_segments(self):
"""Return all segments associated with the span."""
Expand Down

0 comments on commit 57de699

Please sign in to comment.