feat(client,setting): complete Phase 2 with partitioned client_follow_logs
- apps/client (11 models): Client, ClientContact, ClientRequirement, ClientSchoolPreference, ClientFollowLog (partitioned), ClientFollowLogAttachment, ClientViewing, ClientPropertyMatch, ClientStatusLog, ClientFavoriteFolder, ClientFolderItem - apps/client/0002 RunSQL: PARTITION BY RANGE(created_at) for client_follow_logs + monthly partitions + default; triggers update_client_last_follow + update_client_viewing_progress; partial unique index on client_no WHERE deleted_at IS NULL - apps/setting (4 models): LookupGroup, LookupItem, TenantSetting, FieldRequirementRule (tenant schema only per spec) manage.py check green; all 9 Phase 2 apps complete.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
from .contacts import ClientContact, ClientRequirement, ClientSchoolPreference
|
||||
from .core import Client
|
||||
from .folders import ClientFavoriteFolder, ClientFolderItem
|
||||
from .follow import ClientFollowLog, ClientFollowLogAttachment
|
||||
from .viewing_match import (
|
||||
ClientPropertyMatch,
|
||||
ClientStatusLog,
|
||||
ClientViewing,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Client",
|
||||
"ClientContact",
|
||||
"ClientRequirement",
|
||||
"ClientSchoolPreference",
|
||||
"ClientFollowLog",
|
||||
"ClientFollowLogAttachment",
|
||||
"ClientViewing",
|
||||
"ClientPropertyMatch",
|
||||
"ClientStatusLog",
|
||||
"ClientFavoriteFolder",
|
||||
"ClientFolderItem",
|
||||
]
|
||||
|
||||
143
apps/client/models/contacts.py
Normal file
143
apps/client/models/contacts.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
|
||||
from core.enums import (
|
||||
ClientBuildingAgeRange,
|
||||
ClientContactGender,
|
||||
ClientDecoration,
|
||||
ClientFloorPreference,
|
||||
ClientOrientation,
|
||||
ClientRequirementType,
|
||||
)
|
||||
from core.models.base import UUIDPrimaryKeyModel
|
||||
|
||||
|
||||
class ClientContact(UUIDPrimaryKeyModel):
|
||||
client = models.ForeignKey(
|
||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="contacts"
|
||||
)
|
||||
sort_order = models.SmallIntegerField(default=0)
|
||||
name = models.CharField(max_length=50)
|
||||
gender = models.CharField(
|
||||
max_length=10, choices=ClientContactGender.choices, default=ClientContactGender.MALE
|
||||
)
|
||||
|
||||
phone_enc = models.BinaryField()
|
||||
phone_hash = models.CharField(max_length=64)
|
||||
phone_country_code = models.CharField(max_length=10, default="+86")
|
||||
phone_is_invalid = models.BooleanField(default=False)
|
||||
|
||||
phone2_enc = models.BinaryField(null=True, blank=True)
|
||||
phone2_hash = models.CharField(max_length=64, blank=True, default="")
|
||||
|
||||
wechat = models.CharField(max_length=100, blank=True, default="")
|
||||
qq = models.CharField(max_length=20, blank=True, default="")
|
||||
remarks = models.CharField(max_length=200, blank=True, default="")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
created_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_client_contacts",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_contacts"
|
||||
indexes = [
|
||||
models.Index(fields=["phone_hash"], name="idx_cc_phone_hash"),
|
||||
models.Index(fields=["phone2_hash"], name="idx_cc_phone2_hash"),
|
||||
models.Index(fields=["client"], name="idx_cc_client"),
|
||||
]
|
||||
|
||||
|
||||
class ClientRequirement(UUIDPrimaryKeyModel):
|
||||
client = models.ForeignKey(
|
||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="requirements"
|
||||
)
|
||||
requirement_type = models.CharField(
|
||||
max_length=20, choices=ClientRequirementType.choices
|
||||
)
|
||||
is_primary = models.BooleanField(default=True)
|
||||
|
||||
budget_min = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
budget_max = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
area_min = models.DecimalField(
|
||||
max_digits=8, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
area_max = models.DecimalField(
|
||||
max_digits=8, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
|
||||
bedroom_counts = ArrayField(
|
||||
models.SmallIntegerField(), blank=True, default=list
|
||||
)
|
||||
floor_preferences = ArrayField(
|
||||
models.CharField(max_length=20, choices=ClientFloorPreference.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
orientations = ArrayField(
|
||||
models.CharField(max_length=10, choices=ClientOrientation.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
decorations = ArrayField(
|
||||
models.CharField(max_length=10, choices=ClientDecoration.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
building_age_ranges = ArrayField(
|
||||
models.CharField(max_length=20, choices=ClientBuildingAgeRange.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
|
||||
intent_district_ids = ArrayField(
|
||||
models.UUIDField(), blank=True, default=list
|
||||
)
|
||||
intent_business_area_ids = ArrayField(
|
||||
models.UUIDField(), blank=True, default=list
|
||||
)
|
||||
intent_complex_names = models.TextField(blank=True, default="")
|
||||
transportation = models.CharField(max_length=50, blank=True, default="")
|
||||
intent_school_names = models.TextField(blank=True, default="")
|
||||
school_enrollment_date = models.DateField(null=True, blank=True)
|
||||
traffic_preference = models.TextField(blank=True, default="")
|
||||
requirement_notes = models.CharField(max_length=200, blank=True, default="")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_requirements"
|
||||
indexes = [
|
||||
models.Index(fields=["client"], name="idx_creq_client"),
|
||||
models.Index(fields=["requirement_type", "client"], name="idx_creq_type"),
|
||||
models.Index(fields=["budget_min", "budget_max"], name="idx_creq_budget"),
|
||||
models.Index(fields=["area_min", "area_max"], name="idx_creq_area"),
|
||||
]
|
||||
|
||||
|
||||
class ClientSchoolPreference(UUIDPrimaryKeyModel):
|
||||
requirement = models.ForeignKey(
|
||||
ClientRequirement,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="school_preferences",
|
||||
)
|
||||
school_id = models.UUIDField(null=True, blank=True)
|
||||
school_name = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_school_preferences"
|
||||
indexes = [
|
||||
models.Index(fields=["requirement"], name="idx_csp_requirement"),
|
||||
]
|
||||
147
apps/client/models/core.py
Normal file
147
apps/client/models/core.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
|
||||
from core.enums import (
|
||||
ClientActivityLevel,
|
||||
ClientBuyingPurpose,
|
||||
ClientGrade,
|
||||
ClientIdType,
|
||||
ClientInvalidReason,
|
||||
ClientPaymentMethod,
|
||||
ClientPropertiesOwned,
|
||||
ClientPropertyUsage,
|
||||
ClientStatus,
|
||||
ClientTransactedPropertyType,
|
||||
ClientTransactedType,
|
||||
ClientTransferToPublicType,
|
||||
ClientType,
|
||||
)
|
||||
from core.models.base import AuditedModel
|
||||
|
||||
|
||||
class Client(AuditedModel):
|
||||
client_no = models.CharField(max_length=30, unique=True)
|
||||
client_type = models.CharField(
|
||||
max_length=20, choices=ClientType.choices, default=ClientType.PRIVATE
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20, choices=ClientStatus.choices, default=ClientStatus.BUYING
|
||||
)
|
||||
grade = models.CharField(
|
||||
max_length=5, choices=ClientGrade.choices, default=ClientGrade.C
|
||||
)
|
||||
property_usage = models.CharField(
|
||||
max_length=30,
|
||||
choices=ClientPropertyUsage.choices,
|
||||
default=ClientPropertyUsage.RESIDENTIAL,
|
||||
)
|
||||
buying_purpose = ArrayField(
|
||||
models.CharField(max_length=20, choices=ClientBuyingPurpose.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
payment_method = models.CharField(
|
||||
max_length=30, choices=ClientPaymentMethod.choices, blank=True, default=""
|
||||
)
|
||||
properties_owned = models.CharField(
|
||||
max_length=20, choices=ClientPropertiesOwned.choices, blank=True, default=""
|
||||
)
|
||||
has_loan_record = models.BooleanField(null=True, blank=True)
|
||||
|
||||
id_type = models.CharField(
|
||||
max_length=20, choices=ClientIdType.choices, blank=True, default=""
|
||||
)
|
||||
id_number_enc = models.BinaryField(null=True, blank=True)
|
||||
|
||||
source = models.CharField(max_length=50, blank=True, default="")
|
||||
remarks = models.TextField(blank=True, default="")
|
||||
|
||||
is_starred = models.BooleanField(default=False)
|
||||
is_pinned = models.BooleanField(default=False)
|
||||
is_big_value = models.BooleanField(default=False)
|
||||
is_protected = models.BooleanField(default=False)
|
||||
prefers_new_house = models.BooleanField(null=True, blank=True)
|
||||
|
||||
transfer_to_public_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=ClientTransferToPublicType.choices,
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
transferred_public_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
invalid_reason = models.CharField(
|
||||
max_length=30, choices=ClientInvalidReason.choices, blank=True, default=""
|
||||
)
|
||||
invalidated_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
transacted_at = models.DateField(null=True, blank=True)
|
||||
transacted_property = models.ForeignKey(
|
||||
"fonrey_property.Property",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="transacted_clients",
|
||||
)
|
||||
transacted_price = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
transacted_type = models.CharField(
|
||||
max_length=20, choices=ClientTransactedType.choices, blank=True, default=""
|
||||
)
|
||||
transacted_property_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=ClientTransactedPropertyType.choices,
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
|
||||
first_recorder = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="first_recorded_clients",
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="owned_clients",
|
||||
)
|
||||
org_unit = models.ForeignKey(
|
||||
"org.OrgUnit",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="clients",
|
||||
)
|
||||
|
||||
activity_level = models.CharField(
|
||||
max_length=20, choices=ClientActivityLevel.choices, blank=True, default=""
|
||||
)
|
||||
last_active_at = models.DateTimeField(null=True, blank=True)
|
||||
last_follow_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
commission_date = models.DateField(null=True, blank=True)
|
||||
entrust_count = models.SmallIntegerField(default=1)
|
||||
|
||||
version = models.IntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
db_table = "clients"
|
||||
indexes = [
|
||||
models.Index(fields=["client_type", "status"], name="idx_clients_type_stat"),
|
||||
models.Index(fields=["owner"], name="idx_clients_owner"),
|
||||
models.Index(fields=["org_unit"], name="idx_clients_org_unit"),
|
||||
models.Index(
|
||||
fields=["activity_level", "-last_active_at"],
|
||||
name="idx_clients_activity",
|
||||
),
|
||||
models.Index(fields=["grade"], name="idx_clients_grade"),
|
||||
models.Index(
|
||||
fields=["-transferred_public_at"], name="idx_clients_transferred"
|
||||
),
|
||||
models.Index(fields=["-last_follow_at"], name="idx_clients_last_follow"),
|
||||
]
|
||||
48
apps/client/models/folders.py
Normal file
48
apps/client/models/folders.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from django.db import models
|
||||
|
||||
from core.models.base import UUIDPrimaryKeyModel
|
||||
|
||||
|
||||
class ClientFavoriteFolder(UUIDPrimaryKeyModel):
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff", on_delete=models.CASCADE, related_name="favorite_folders"
|
||||
)
|
||||
name = models.CharField(max_length=10)
|
||||
is_default = models.BooleanField(default=False)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_favorite_folders"
|
||||
indexes = [
|
||||
models.Index(fields=["staff"], name="idx_cff_staff"),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["staff"],
|
||||
condition=models.Q(is_default=True, deleted_at__isnull=True),
|
||||
name="uq_cff_default_per_staff",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class ClientFolderItem(models.Model):
|
||||
folder = models.ForeignKey(
|
||||
ClientFavoriteFolder, on_delete=models.CASCADE, related_name="items"
|
||||
)
|
||||
client = models.ForeignKey(
|
||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="folder_items"
|
||||
)
|
||||
added_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_folder_items"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["folder", "client"], name="uq_cfi_folder_client"
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=["client"], name="idx_cfi_client"),
|
||||
]
|
||||
55
apps/client/models/follow.py
Normal file
55
apps/client/models/follow.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from django.db import models
|
||||
|
||||
from core.enums import ClientFollowLogType
|
||||
from core.models.base import UUIDPrimaryKeyModel
|
||||
|
||||
|
||||
class ClientFollowLog(models.Model):
|
||||
"""Partitioned table (PARTITION BY RANGE created_at).
|
||||
|
||||
Managed via RunSQL; Django ORM treats parent as unmanaged.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True)
|
||||
created_at = models.DateTimeField()
|
||||
client = models.ForeignKey(
|
||||
"fonrey_client.Client",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="follow_logs",
|
||||
)
|
||||
|
||||
log_type = models.CharField(max_length=30, choices=ClientFollowLogType.choices)
|
||||
purpose = models.CharField(max_length=50, blank=True, default="")
|
||||
content = models.TextField(blank=True, default="")
|
||||
log_tag = models.CharField(max_length=50, blank=True, default="")
|
||||
change_detail = models.JSONField(null=True, blank=True)
|
||||
|
||||
is_public = models.BooleanField(default=True)
|
||||
is_deletable = models.BooleanField(default=True)
|
||||
|
||||
operator = models.ForeignKey(
|
||||
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
operator_snapshot = models.JSONField(null=True, blank=True)
|
||||
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_follow_logs"
|
||||
managed = False
|
||||
unique_together = (("id", "created_at"),)
|
||||
|
||||
|
||||
class ClientFollowLogAttachment(UUIDPrimaryKeyModel):
|
||||
follow_log_id = models.UUIDField() # cross-partitioned FK; not enforced via Django FK
|
||||
file_key = models.TextField()
|
||||
file_name = models.CharField(max_length=255)
|
||||
file_size = models.IntegerField()
|
||||
file_type = models.CharField(max_length=10, blank=True, default="")
|
||||
has_location = models.BooleanField(default=False)
|
||||
sort_order = models.SmallIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_follow_log_attachments"
|
||||
indexes = [models.Index(fields=["follow_log_id"], name="idx_cfla_log")]
|
||||
147
apps/client/models/viewing_match.py
Normal file
147
apps/client/models/viewing_match.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
|
||||
from core.enums import (
|
||||
ClientPropertyMatchGroup,
|
||||
ClientPropertyMatchSource,
|
||||
ClientPropertyMatchStatus,
|
||||
ClientStatusLogChangeType,
|
||||
ClientViewingIntent,
|
||||
ClientViewingType,
|
||||
)
|
||||
from core.models.base import UUIDPrimaryKeyModel
|
||||
|
||||
|
||||
class ClientViewing(UUIDPrimaryKeyModel):
|
||||
client = models.ForeignKey(
|
||||
"fonrey_client.Client", on_delete=models.RESTRICT, related_name="viewings"
|
||||
)
|
||||
property = models.ForeignKey(
|
||||
"fonrey_property.Property",
|
||||
on_delete=models.RESTRICT,
|
||||
related_name="client_viewings",
|
||||
)
|
||||
viewing_type = models.CharField(
|
||||
max_length=20, choices=ClientViewingType.choices, default=ClientViewingType.VIEWING
|
||||
)
|
||||
|
||||
agent = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="led_viewings",
|
||||
)
|
||||
companion_ids = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
cooperator_ids = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
|
||||
scheduled_at = models.DateTimeField(null=True, blank=True)
|
||||
viewing_start_at = models.DateTimeField(null=True, blank=True)
|
||||
viewing_end_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
situation = models.TextField(blank=True, default="")
|
||||
client_intent = models.CharField(
|
||||
max_length=20, choices=ClientViewingIntent.choices, blank=True, default=""
|
||||
)
|
||||
viewing_progress = models.SmallIntegerField(null=True, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
created_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_client_viewings",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_viewings"
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["client", "-viewing_start_at"], name="idx_cv_client_time"
|
||||
),
|
||||
models.Index(fields=["property"], name="idx_cv_property"),
|
||||
models.Index(fields=["agent"], name="idx_cv_agent"),
|
||||
]
|
||||
|
||||
|
||||
class ClientPropertyMatch(UUIDPrimaryKeyModel):
|
||||
client = models.ForeignKey(
|
||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="property_matches"
|
||||
)
|
||||
property = models.ForeignKey(
|
||||
"fonrey_property.Property",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="client_matches",
|
||||
)
|
||||
|
||||
match_source = models.CharField(
|
||||
max_length=20,
|
||||
choices=ClientPropertyMatchSource.choices,
|
||||
default=ClientPropertyMatchSource.RECORDED,
|
||||
)
|
||||
match_group = models.CharField(
|
||||
max_length=30, choices=ClientPropertyMatchGroup.choices, blank=True, default=""
|
||||
)
|
||||
match_score = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
match_reasons = models.JSONField(null=True, blank=True)
|
||||
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=ClientPropertyMatchStatus.choices,
|
||||
default=ClientPropertyMatchStatus.SUGGESTED,
|
||||
)
|
||||
shared_at = models.DateTimeField(null=True, blank=True)
|
||||
feedback = models.CharField(max_length=50, blank=True, default="")
|
||||
calculated_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_matches",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_property_matches"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["client", "property"], name="uq_client_match_pair"
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["client", "match_source", "match_group"],
|
||||
name="idx_cpm_client_grp",
|
||||
),
|
||||
models.Index(fields=["client", "status"], name="idx_cpm_status"),
|
||||
]
|
||||
|
||||
|
||||
class ClientStatusLog(models.Model):
|
||||
"""Audit log; record-level immutable (no deleted_at)."""
|
||||
|
||||
id = models.UUIDField(primary_key=True)
|
||||
client = models.ForeignKey(
|
||||
"fonrey_client.Client", on_delete=models.RESTRICT, related_name="status_logs"
|
||||
)
|
||||
change_type = models.CharField(
|
||||
max_length=30, choices=ClientStatusLogChangeType.choices
|
||||
)
|
||||
old_value = models.JSONField(null=True, blank=True)
|
||||
new_value = models.JSONField(null=True, blank=True)
|
||||
reason = models.TextField(blank=True, default="")
|
||||
operator = models.ForeignKey(
|
||||
"org.Staff", on_delete=models.RESTRICT, related_name="client_status_changes"
|
||||
)
|
||||
operated_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_status_logs"
|
||||
indexes = [
|
||||
models.Index(fields=["client", "-operated_at"], name="idx_csl_client"),
|
||||
models.Index(fields=["change_type", "-operated_at"], name="idx_csl_type"),
|
||||
]
|
||||
Reference in New Issue
Block a user