feat(property): add 23-table property module with partitioned follow_logs and property_photos

This commit is contained in:
2026-04-29 17:27:15 +08:00
parent c57462f6d1
commit 5b55dda367
8 changed files with 1707 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
from apps.property.models.core import (
Property,
PropertyCertificate,
PropertyCompleteness,
PropertyContact,
PropertyMarketing,
PropertyProtection,
)
from apps.property.models.follow_keys import (
FollowLog,
FollowLogAttachment,
FollowLogRecording,
KeyAttachment,
PropertyKey,
)
from apps.property.models.listings import (
Commission,
CommissionAttachment,
ListingHistory,
NumberHolderApproval,
PriceChange,
)
from apps.property.models.media import (
FieldSurvey,
PropertyAttachment,
PropertyFavorite,
PropertyPhoto,
PropertyTag,
PropertyTagRelation,
SurveyPhoto,
)
__all__ = [
"Commission",
"CommissionAttachment",
"FieldSurvey",
"FollowLog",
"FollowLogAttachment",
"FollowLogRecording",
"KeyAttachment",
"ListingHistory",
"NumberHolderApproval",
"PriceChange",
"Property",
"PropertyAttachment",
"PropertyCertificate",
"PropertyCompleteness",
"PropertyContact",
"PropertyFavorite",
"PropertyKey",
"PropertyMarketing",
"PropertyPhoto",
"PropertyProtection",
"PropertyTag",
"PropertyTagRelation",
"SurveyPhoto",
]

View File

@@ -0,0 +1,338 @@
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField
from django.db import models
from core.enums import (
PropertyAttribute,
PropertyContactGender,
PropertyContactIdentity,
PropertyDecoration,
PropertyGrade,
PropertyHouseStatus,
PropertyOrientation,
PropertyOwnershipNature,
PropertyPaymentMethod,
PropertyShopLocation,
PropertyStatus,
PropertyTaxIncluded,
PropertyType,
PropertyViewingTime,
)
from core.models.base import SoftDeleteModel, TimeStampedModel, UUIDPrimaryKeyModel
class Property(SoftDeleteModel):
property_type = models.CharField(max_length=30, choices=PropertyType.choices)
status = models.CharField(
max_length=20,
choices=PropertyStatus.choices,
default=PropertyStatus.FOR_SALE,
)
attribute = models.CharField(
max_length=10,
choices=PropertyAttribute.choices,
default=PropertyAttribute.PUBLIC,
)
private_reason = models.TextField(blank=True, default="")
complex = models.ForeignKey(
"fonrey_complex.Complex",
on_delete=models.RESTRICT,
related_name="properties",
)
building = models.ForeignKey(
"fonrey_complex.Building",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="properties",
)
block_no = models.CharField(max_length=30, blank=True, default="")
unit_no = models.CharField(max_length=30, blank=True, default="")
room_no = models.CharField(max_length=30, blank=True, default="")
floor = models.SmallIntegerField()
total_floors = models.SmallIntegerField()
bedroom_count = models.SmallIntegerField(default=0)
living_room_count = models.SmallIntegerField(default=0)
bathroom_count = models.SmallIntegerField(default=0)
kitchen_count = models.SmallIntegerField(default=0)
balcony_count = models.SmallIntegerField(default=0)
area = models.DecimalField(max_digits=8, decimal_places=2)
inner_area = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
sale_bottom_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
sale_record_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
orientation = models.CharField(
max_length=15, blank=True, default="", choices=PropertyOrientation.choices
)
decoration = models.CharField(
max_length=10, blank=True, default="", choices=PropertyDecoration.choices
)
has_elevator = models.BooleanField(null=True, blank=True)
built_year = models.SmallIntegerField(null=True, blank=True)
usage_type = models.CharField(max_length=30, blank=True, default="")
usage_subtype = models.CharField(max_length=30, blank=True, default="")
shop_frontage = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
shop_depth = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
shop_height = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
shop_location = models.CharField(
max_length=20, blank=True, default="", choices=PropertyShopLocation.choices
)
house_status = models.CharField(
max_length=20, blank=True, default="", choices=PropertyHouseStatus.choices
)
viewing_time = models.CharField(
max_length=20, blank=True, default="", choices=PropertyViewingTime.choices
)
grade = models.CharField(max_length=2, blank=True, default="", choices=PropertyGrade.choices)
ownership_years = models.CharField(max_length=30, blank=True, default="")
ownership_years_detail = models.CharField(max_length=20, blank=True, default="")
ownership_nature = models.CharField(
max_length=20, blank=True, default="", choices=PropertyOwnershipNature.choices
)
is_only_house = models.BooleanField(null=True, blank=True)
payment_method = models.CharField(
max_length=15, blank=True, default="", choices=PropertyPaymentMethod.choices
)
tax_included = models.CharField(
max_length=15, blank=True, default="", choices=PropertyTaxIncluded.choices
)
has_mortgage = models.BooleanField(null=True, blank=True)
has_loan = models.BooleanField(null=True, blank=True)
has_seal = models.BooleanField(null=True, blank=True)
has_restriction = models.BooleanField(null=True, blank=True)
original_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
sale_reason = models.TextField(blank=True, default="")
remarks = models.TextField(blank=True, default="")
first_recorder = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="first_recorded_properties",
)
number_holder = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="held_properties",
)
seller_agent = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="selling_properties",
)
buyer_agent = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="buying_properties",
)
source = models.CharField(max_length=50, blank=True, default="")
completeness_score = models.SmallIntegerField(default=0)
listed_at = models.DateTimeField(null=True, blank=True)
last_followed_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_properties",
)
updated_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="updated_properties",
)
search_vector = SearchVectorField(null=True, blank=True)
version = models.IntegerField(default=1)
class Meta:
db_table = "properties"
constraints = [
models.CheckConstraint(
check=models.Q(floor__gt=0) & models.Q(floor__lte=models.F("total_floors")),
name="chk_property_floor",
),
]
indexes = [
GinIndex(fields=["search_vector"], name="idx_properties_search"),
models.Index(fields=["complex"], name="idx_properties_complex"),
models.Index(fields=["status"], name="idx_properties_status"),
models.Index(fields=["sale_price"], name="idx_properties_sale_price"),
models.Index(fields=["area"], name="idx_properties_area"),
models.Index(fields=["listed_at"], name="idx_properties_listed_at"),
models.Index(fields=["last_followed_at"], name="idx_properties_last_followed"),
models.Index(fields=["bedroom_count"], name="idx_properties_bedroom"),
models.Index(fields=["grade"], name="idx_properties_grade"),
models.Index(fields=["completeness_score"], name="idx_properties_completeness"),
models.Index(fields=["seller_agent"], name="idx_properties_seller_agent"),
models.Index(fields=["number_holder"], name="idx_properties_number_holder"),
models.Index(
fields=["status", "attribute", "complex", "sale_price"],
name="idx_properties_list_composite",
),
models.Index(
fields=["seller_agent", "status", "listed_at"],
name="idx_properties_my_properties",
),
]
class PropertyContact(SoftDeleteModel):
property = models.ForeignKey(
Property, on_delete=models.CASCADE, related_name="contacts"
)
name = models.CharField(max_length=50)
gender = models.CharField(
max_length=10, choices=PropertyContactGender.choices, default=PropertyContactGender.MALE
)
identity = models.CharField(
max_length=20,
choices=PropertyContactIdentity.choices,
default=PropertyContactIdentity.CONTACT,
)
phone_enc = models.BinaryField()
phone_hash = models.CharField(max_length=64)
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.TextField(blank=True, default="")
is_number_holder = models.BooleanField(default=False)
number_holder_approved_at = models.DateTimeField(null=True, blank=True)
sort_order = models.IntegerField(default=0)
created_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_property_contacts",
)
updated_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="updated_property_contacts",
)
class Meta:
db_table = "property_contacts"
indexes = [
models.Index(fields=["property"], name="idx_pc_property"),
models.Index(fields=["phone_hash"], name="idx_pc_phone_hash"),
models.Index(fields=["phone2_hash"], name="idx_pc_phone2_hash"),
]
class PropertyMarketing(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="marketing"
)
marketing_title = models.CharField(max_length=30, blank=True, default="")
core_selling_points = models.TextField(blank=True, default="")
owner_attitude = models.TextField(blank=True, default="")
layout_description = models.TextField(blank=True, default="")
complex_description = models.TextField(blank=True, default="")
ai_generated_points = models.BooleanField(default=False)
ai_generated_attitude = models.BooleanField(default=False)
updated_at = models.DateTimeField(auto_now=True)
updated_by = models.ForeignKey(
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
)
class Meta:
db_table = "property_marketing"
class PropertyCertificate(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="certificate"
)
owner_name = models.CharField(max_length=100, blank=True, default="")
owner_id_number = models.CharField(max_length=50, blank=True, default="")
owner_cert_type = models.CharField(max_length=20, blank=True, default="")
property_location = models.CharField(max_length=500, blank=True, default="")
cert_status = models.CharField(max_length=30, blank=True, default="")
cert_no = models.CharField(max_length=100, blank=True, default="")
first_registered_at = models.DateField(null=True, blank=True)
ownership_nature = models.CharField(max_length=30, blank=True, default="")
land_nature = models.CharField(max_length=30, blank=True, default="")
updated_at = models.DateTimeField(auto_now=True)
updated_by = models.ForeignKey(
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
)
class Meta:
db_table = "property_certificates"
class PropertyCompleteness(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="completeness"
)
score_core_info = models.SmallIntegerField(default=0)
score_attachment = models.SmallIntegerField(default=0)
score_survey = models.SmallIntegerField(default=0)
score_vr = models.SmallIntegerField(default=0)
score_key = models.SmallIntegerField(default=0)
score_commission = models.SmallIntegerField(default=0)
score_verification = models.SmallIntegerField(default=0)
score_follow_up = models.SmallIntegerField(default=0)
score_viewing = models.SmallIntegerField(default=0)
score_other = models.SmallIntegerField(default=0)
total_score = models.SmallIntegerField(default=0)
calculated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "property_completeness"
class PropertyProtection(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="protection"
)
is_protected = models.BooleanField(default=False)
reason = models.TextField(blank=True, default="")
start_at = models.DateTimeField(null=True, blank=True)
end_at = models.DateTimeField(null=True, blank=True)
set_by = models.ForeignKey(
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "property_protections"

View File

@@ -0,0 +1,130 @@
from django.db import models
from core.enums import (
PropertyFollowAiTag,
PropertyFollowAttachmentFileType,
PropertyFollowLogType,
PropertyKeyType,
)
from core.models.base import UUIDPrimaryKeyModel
class FollowLog(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()
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="follow_logs"
)
log_type = models.CharField(max_length=30, choices=PropertyFollowLogType.choices)
purpose = models.CharField(max_length=50, blank=True, default="")
content = models.TextField(blank=True, default="")
ai_tag = models.CharField(
max_length=20, blank=True, default="", choices=PropertyFollowAiTag.choices
)
change_detail = models.JSONField(null=True, blank=True)
log_tag = models.CharField(max_length=50, blank=True, default="")
is_public = 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)
is_deletable = models.BooleanField(default=True)
deleted_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "follow_logs"
managed = False
unique_together = (("id", "created_at"),)
class FollowLogAttachment(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="", choices=PropertyFollowAttachmentFileType.choices
)
sort_order = models.SmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "follow_log_attachments"
indexes = [models.Index(fields=["follow_log_id"], name="idx_fla_log")]
class FollowLogRecording(UUIDPrimaryKeyModel):
follow_log_id = models.UUIDField()
file_key = models.TextField()
duration_seconds = models.IntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "follow_log_recordings"
indexes = [models.Index(fields=["follow_log_id"], name="idx_flr_log")]
class PropertyKey(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="keys"
)
key_type = models.CharField(max_length=20, choices=PropertyKeyType.choices)
holder = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="held_keys",
)
holder_snapshot = models.JSONField(null=True, blank=True)
storage_unit = models.ForeignKey(
"org.OrgUnit",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="stored_keys",
)
is_other_agency = models.BooleanField(default=False)
other_agency_info = models.CharField(max_length=30, blank=True, default="")
remarks = models.TextField(blank=True, default="")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_property_keys",
)
class Meta:
db_table = "property_keys"
indexes = [models.Index(fields=["property"], name="idx_pk_property")]
class KeyAttachment(UUIDPrimaryKeyModel):
key = models.ForeignKey(
PropertyKey, on_delete=models.CASCADE, related_name="attachments"
)
file_key = models.TextField()
file_name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "key_attachments"
indexes = [models.Index(fields=["key"], name="idx_ka_key")]

View File

@@ -0,0 +1,194 @@
from django.db import models
from core.enums import (
PropertyCommissionAttachmentCategory,
PropertyCommissionOwnerType,
PropertyCommissionStatus,
PropertyListingHistoryStatus,
PropertyListingType,
PropertyNumberHolderApprovalStatus,
)
from core.models.base import TimeStampedModel, UUIDPrimaryKeyModel
class ListingHistory(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property",
on_delete=models.RESTRICT,
related_name="listing_histories",
)
listing_type = models.CharField(max_length=20, choices=PropertyListingType.choices)
status = models.CharField(
max_length=10,
choices=PropertyListingHistoryStatus.choices,
default=PropertyListingHistoryStatus.ACTIVE,
)
sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
sale_unit_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
ownership_years = models.CharField(max_length=30, blank=True, default="")
is_only_house = models.BooleanField(null=True, blank=True)
tax_included = models.CharField(max_length=15, blank=True, default="")
sale_reason = models.TextField(blank=True, default="")
seller_agent = models.ForeignKey(
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
)
seller_agent_snapshot = models.JSONField(null=True, blank=True)
started_at = models.DateTimeField(auto_now_add=False)
ended_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "listing_histories"
indexes = [
models.Index(fields=["property"], name="idx_lh_property"),
models.Index(fields=["property", "status"], name="idx_lh_active"),
]
class PriceChange(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.RESTRICT, related_name="price_changes"
)
old_sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
new_sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
old_bottom_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
new_bottom_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
old_record_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
new_record_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
old_rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
new_rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
change_reason = models.TextField()
changed_at = models.DateTimeField(auto_now_add=True)
changed_by = models.ForeignKey("org.Staff", on_delete=models.RESTRICT)
class Meta:
db_table = "price_changes"
indexes = [
models.Index(fields=["property"], name="idx_pchg_property"),
models.Index(fields=["property", "-changed_at"], name="idx_pchg_time"),
]
class Commission(TimeStampedModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="commissions"
)
commission_type = models.CharField(max_length=50)
period_start = models.DateField()
period_end = models.DateField(null=True, blank=True)
is_open_ended = models.BooleanField(default=False)
agent = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="commissions_as_agent",
)
agent_snapshot = models.JSONField(null=True, blank=True)
signing_method = models.CharField(max_length=50, blank=True, default="")
owner_type = models.CharField(
max_length=20,
choices=PropertyCommissionOwnerType.choices,
default=PropertyCommissionOwnerType.OWNER,
)
property_owner_contact = models.ForeignKey(
"fonrey_property.PropertyContact",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="commissions",
)
owner_name = models.CharField(max_length=50, blank=True, default="")
owner_id_type = models.CharField(max_length=20, blank=True, default="")
owner_id_number = models.CharField(max_length=50, blank=True, default="")
owner_id_number_enc = models.BinaryField(null=True, blank=True)
remarks = models.TextField(blank=True, default="")
status = models.CharField(
max_length=20,
choices=PropertyCommissionStatus.choices,
default=PropertyCommissionStatus.ACTIVE,
)
created_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_commissions",
)
class Meta:
db_table = "commissions"
indexes = [
models.Index(fields=["property"], name="idx_commissions_property"),
models.Index(fields=["property", "status"], name="idx_commissions_active"),
]
class CommissionAttachment(UUIDPrimaryKeyModel):
commission = models.ForeignKey(
Commission, on_delete=models.CASCADE, related_name="attachments"
)
category = models.CharField(
max_length=20, choices=PropertyCommissionAttachmentCategory.choices
)
file_key = models.TextField()
file_name = models.CharField(max_length=255)
file_size = models.IntegerField(null=True, blank=True)
sort_order = models.SmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "commission_attachments"
indexes = [models.Index(fields=["commission"], name="idx_ca_commission")]
class NumberHolderApproval(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="number_holder_approvals",
)
contact = models.ForeignKey(
"fonrey_property.PropertyContact",
on_delete=models.CASCADE,
related_name="number_holder_approvals",
)
applicant = models.ForeignKey(
"org.Staff", on_delete=models.RESTRICT, related_name="nh_applications"
)
approver = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="nh_approvals",
)
status = models.CharField(
max_length=20,
choices=PropertyNumberHolderApprovalStatus.choices,
default=PropertyNumberHolderApprovalStatus.PENDING,
)
remarks = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
decided_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "number_holder_approvals"
indexes = [
models.Index(fields=["status"], name="idx_nha_status"),
models.Index(fields=["property"], name="idx_nha_property"),
]

View File

@@ -0,0 +1,166 @@
from django.db import models
from core.enums import (
PropertyAttachmentCategory,
PropertyFieldSurveyStatus,
PropertyPhotoCategory,
PropertySurveyPhotoCategory,
)
from core.models.base import UUIDPrimaryKeyModel
class FieldSurvey(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="field_surveys"
)
status = models.CharField(
max_length=10,
choices=PropertyFieldSurveyStatus.choices,
default=PropertyFieldSurveyStatus.DRAFT,
)
gps_latitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
gps_longitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
gps_accuracy = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
description = models.TextField(blank=True, default="")
submitted_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey("org.Staff", on_delete=models.RESTRICT)
class Meta:
db_table = "field_surveys"
indexes = [
models.Index(fields=["property"], name="idx_fs_property"),
models.Index(fields=["property", "status"], name="idx_fs_submitted"),
]
class SurveyPhoto(UUIDPrimaryKeyModel):
survey = models.ForeignKey(FieldSurvey, on_delete=models.CASCADE, related_name="photos")
category = models.CharField(max_length=20, choices=PropertySurveyPhotoCategory.choices)
file_key = models.TextField()
thumbnail_key = models.TextField(blank=True, default="")
file_size = models.IntegerField(null=True, blank=True)
width = models.IntegerField(null=True, blank=True)
height = models.IntegerField(null=True, blank=True)
sort_order = models.SmallIntegerField(default=0)
is_vr_screenshot = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "survey_photos"
indexes = [
models.Index(fields=["survey"], name="idx_sp_survey"),
models.Index(fields=["survey", "category"], name="idx_sp_category"),
]
class PropertyPhoto(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()
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="photos"
)
category = models.CharField(max_length=20, choices=PropertyPhotoCategory.choices)
file_key = models.TextField()
thumbnail_key = models.TextField(blank=True, default="")
file_name = models.CharField(max_length=255, blank=True, default="")
file_size = models.IntegerField(null=True, blank=True)
width = models.IntegerField(null=True, blank=True)
height = models.IntegerField(null=True, blank=True)
is_cover = models.BooleanField(default=False)
sort_order = models.SmallIntegerField(default=0)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
)
class Meta:
db_table = "property_photos"
managed = False
unique_together = (("id", "created_at"),)
class PropertyAttachment(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="attachments"
)
category = models.CharField(
max_length=20,
choices=PropertyAttachmentCategory.choices,
default=PropertyAttachmentCategory.OTHER,
)
file_key = models.TextField()
file_name = models.CharField(max_length=255)
file_size = models.IntegerField()
file_type = models.CharField(max_length=50, blank=True, default="")
sort_order = models.SmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
)
class Meta:
db_table = "property_attachments"
indexes = [
models.Index(fields=["property"], name="idx_pa_property"),
models.Index(fields=["property", "category"], name="idx_pa_category"),
]
class PropertyTag(UUIDPrimaryKeyModel):
name = models.CharField(max_length=50)
color = models.CharField(max_length=7, blank=True, default="")
is_system = models.BooleanField(default=False)
sort_order = models.IntegerField(default=0)
is_active = models.BooleanField(default=True)
class Meta:
db_table = "property_tags"
class PropertyTagRelation(models.Model):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="tag_relations"
)
tag = models.ForeignKey(
PropertyTag, on_delete=models.CASCADE, related_name="property_relations"
)
class Meta:
db_table = "property_tag_relations"
constraints = [
models.UniqueConstraint(fields=["property", "tag"], name="uq_ptr_property_tag"),
]
indexes = [
models.Index(fields=["property"], name="idx_ptr_property"),
models.Index(fields=["tag"], name="idx_ptr_tag"),
]
class PropertyFavorite(models.Model):
staff = models.ForeignKey(
"org.Staff", on_delete=models.CASCADE, related_name="favorite_properties"
)
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="favorited_by"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "property_favorites"
constraints = [
models.UniqueConstraint(fields=["staff", "property"], name="uq_pfav_staff_property"),
]
indexes = [models.Index(fields=["staff"], name="idx_pfav_staff")]