1. from django.db import models
    
  2. from django.db.models.fields.related import ReverseManyToOneDescriptor
    
  3. from django.db.models.lookups import StartsWith
    
  4. from django.db.models.query_utils import PathInfo
    
  5. 
    
  6. 
    
  7. class CustomForeignObjectRel(models.ForeignObjectRel):
    
  8.     """
    
  9.     Define some extra Field methods so this Rel acts more like a Field, which
    
  10.     lets us use ReverseManyToOneDescriptor in both directions.
    
  11.     """
    
  12. 
    
  13.     @property
    
  14.     def foreign_related_fields(self):
    
  15.         return tuple(lhs_field for lhs_field, rhs_field in self.field.related_fields)
    
  16. 
    
  17.     def get_attname(self):
    
  18.         return self.name
    
  19. 
    
  20. 
    
  21. class StartsWithRelation(models.ForeignObject):
    
  22.     """
    
  23.     A ForeignObject that uses StartsWith operator in its joins instead of
    
  24.     the default equality operator. This is logically a many-to-many relation
    
  25.     and creates a ReverseManyToOneDescriptor in both directions.
    
  26.     """
    
  27. 
    
  28.     auto_created = False
    
  29. 
    
  30.     many_to_many = False
    
  31.     many_to_one = True
    
  32.     one_to_many = False
    
  33.     one_to_one = False
    
  34. 
    
  35.     rel_class = CustomForeignObjectRel
    
  36. 
    
  37.     def __init__(self, *args, **kwargs):
    
  38.         kwargs["on_delete"] = models.DO_NOTHING
    
  39.         super().__init__(*args, **kwargs)
    
  40. 
    
  41.     @property
    
  42.     def field(self):
    
  43.         """
    
  44.         Makes ReverseManyToOneDescriptor work in both directions.
    
  45.         """
    
  46.         return self.remote_field
    
  47. 
    
  48.     def get_extra_restriction(self, alias, related_alias):
    
  49.         to_field = self.remote_field.model._meta.get_field(self.to_fields[0])
    
  50.         from_field = self.model._meta.get_field(self.from_fields[0])
    
  51.         return StartsWith(to_field.get_col(alias), from_field.get_col(related_alias))
    
  52. 
    
  53.     def get_joining_columns(self, reverse_join=False):
    
  54.         return ()
    
  55. 
    
  56.     def get_path_info(self, filtered_relation=None):
    
  57.         to_opts = self.remote_field.model._meta
    
  58.         from_opts = self.model._meta
    
  59.         return [
    
  60.             PathInfo(
    
  61.                 from_opts=from_opts,
    
  62.                 to_opts=to_opts,
    
  63.                 target_fields=(to_opts.pk,),
    
  64.                 join_field=self,
    
  65.                 m2m=False,
    
  66.                 direct=False,
    
  67.                 filtered_relation=filtered_relation,
    
  68.             )
    
  69.         ]
    
  70. 
    
  71.     def get_reverse_path_info(self, filtered_relation=None):
    
  72.         to_opts = self.model._meta
    
  73.         from_opts = self.remote_field.model._meta
    
  74.         return [
    
  75.             PathInfo(
    
  76.                 from_opts=from_opts,
    
  77.                 to_opts=to_opts,
    
  78.                 target_fields=(to_opts.pk,),
    
  79.                 join_field=self.remote_field,
    
  80.                 m2m=False,
    
  81.                 direct=False,
    
  82.                 filtered_relation=filtered_relation,
    
  83.             )
    
  84.         ]
    
  85. 
    
  86.     def contribute_to_class(self, cls, name, private_only=False):
    
  87.         super().contribute_to_class(cls, name, private_only)
    
  88.         setattr(cls, self.name, ReverseManyToOneDescriptor(self))
    
  89. 
    
  90. 
    
  91. class BrokenContainsRelation(StartsWithRelation):
    
  92.     """
    
  93.     This model is designed to yield no join conditions and
    
  94.     raise an exception in ``Join.as_sql()``.
    
  95.     """
    
  96. 
    
  97.     def get_extra_restriction(self, alias, related_alias):
    
  98.         return None
    
  99. 
    
  100. 
    
  101. class SlugPage(models.Model):
    
  102.     slug = models.CharField(max_length=20, unique=True)
    
  103.     descendants = StartsWithRelation(
    
  104.         "self",
    
  105.         from_fields=["slug"],
    
  106.         to_fields=["slug"],
    
  107.         related_name="ascendants",
    
  108.     )
    
  109.     containers = BrokenContainsRelation(
    
  110.         "self",
    
  111.         from_fields=["slug"],
    
  112.         to_fields=["slug"],
    
  113.     )
    
  114. 
    
  115.     class Meta:
    
  116.         ordering = ["slug"]
    
  117. 
    
  118.     def __str__(self):
    
  119.         return "SlugPage %s" % self.slug