checking in for production

This commit is contained in:
2025-10-15 15:07:24 -05:00
parent 2239c861b1
commit cf9773f74d
35 changed files with 2779 additions and 352 deletions

View File

@@ -9,14 +9,24 @@ from core.models import (
Video, Video,
UserVideoProgress, UserVideoProgress,
Conversation, Conversation,
Offer, OfferDocument,
PropertyPictures, PropertyPictures,
OpenHouse, OpenHouse,
PropertySaleInfo, PropertySaleInfo,
PropertyTaxInfo, PropertyTaxInfo,
PropertyWalkScoreInfo, PropertyWalkScoreInfo,
SchoolInfo, SchoolInfo,
Attorney, RealEstateAgent, UserViewModel, PropertySave Attorney,
RealEstateAgent,
UserViewModel,
PropertySave,
Document,
SellerDisclosure,
VendorPictures,
SupportAgent,
SupportCase,
SupportMessage,
FAQ,
) )
@@ -58,10 +68,6 @@ class VendorAdmin(admin.ModelAdmin):
) )
class VideoCategoryAdmin(admin.ModelAdmin): class VideoCategoryAdmin(admin.ModelAdmin):
model = VideoCategory model = VideoCategory
list_display = ("name", "description") list_display = ("name", "description")
@@ -89,8 +95,8 @@ class ConversationAdmin(admin.ModelAdmin):
class OfferAdmin(admin.ModelAdmin): class OfferAdmin(admin.ModelAdmin):
model = Offer model = OfferDocument
list_display = ("pk", "user", "property", "status") list_display = ("pk", "offer_price", "closing_days", "closing_date", "status")
search_fields = ("user", "status") search_fields = ("user", "status")
@@ -118,15 +124,26 @@ class PropertyWalkScoreInfoAdmin(admin.ModelAdmin):
class SchoolInfoAdmin(admin.ModelAdmin): class SchoolInfoAdmin(admin.ModelAdmin):
model = SchoolInfo model = SchoolInfo
class PropertyWalkScoreInfoStackedInline(admin.StackedInline): class PropertyWalkScoreInfoStackedInline(admin.StackedInline):
model = PropertyWalkScoreInfo model = PropertyWalkScoreInfo
class SchoolInfoStackedInline(admin.StackedInline): class SchoolInfoStackedInline(admin.StackedInline):
model = SchoolInfo model = SchoolInfo
class PropertyAdmin(admin.ModelAdmin): class PropertyAdmin(admin.ModelAdmin):
model = Property model = Property
list_display = ("pk", "owner", "address", "city", "state", "zip_code") list_display = (
"pk",
"owner",
"address",
"city",
"state",
"zip_code",
"property_status",
)
search_fields = ("address", "city", "state", "zip_code", "owner") search_fields = ("address", "city", "state", "zip_code", "owner")
inlines = [PropertyWalkScoreInfoStackedInline, SchoolInfoStackedInline] inlines = [PropertyWalkScoreInfoStackedInline, SchoolInfoStackedInline]
@@ -134,63 +151,182 @@ class PropertyAdmin(admin.ModelAdmin):
# Registering the new Attorney model # Registering the new Attorney model
@admin.register(Attorney) @admin.register(Attorney)
class AttorneyAdmin(admin.ModelAdmin): class AttorneyAdmin(admin.ModelAdmin):
list_display = ('user', 'firm_name', 'bar_number', 'phone_number', 'city', 'state', 'years_experience') list_display = (
list_filter = ('state', 'years_experience', 'specialties') # You might need custom list_filter for JSONField "user",
search_fields = ('user__email', 'user__first_name', 'user__last_name', 'firm_name', 'bar_number', 'city') "firm_name",
"bar_number",
"phone_number",
"city",
"state",
"years_experience",
)
list_filter = (
"state",
"years_experience",
"specialties",
) # You might need custom list_filter for JSONField
search_fields = (
"user__email",
"user__first_name",
"user__last_name",
"firm_name",
"bar_number",
"city",
)
# Use filter_horizontal for JSONField if you want a nice interface for many-to-many like selection # Use filter_horizontal for JSONField if you want a nice interface for many-to-many like selection
# filter_horizontal = ('specialties', 'licensed_states') # filter_horizontal = ('specialties', 'licensed_states')
fieldsets = ( fieldsets = (
(None, { (
'fields': ('user', 'firm_name', 'bar_number', 'phone_number', 'address', 'city', 'state', 'zip_code', 'bio', 'profile_picture', 'website') None,
}), {
('Professional Details', { "fields": (
'fields': ('specialties', 'years_experience', 'licensed_states') "user",
}), "firm_name",
('Location', { "bar_number",
'fields': ('latitude', 'longitude'), "phone_number",
}), "address",
('Timestamps', { "city",
'fields': ('created_at', 'updated_at'), "state",
'classes': ('collapse',) "zip_code",
}), "bio",
"profile_picture",
"website",
)
},
),
(
"Professional Details",
{"fields": ("specialties", "years_experience", "licensed_states")},
),
(
"Location",
{
"fields": ("latitude", "longitude"),
},
),
(
"Timestamps",
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
),
) )
readonly_fields = ('created_at', 'updated_at') readonly_fields = ("created_at", "updated_at")
# Registering the new RealEstateAgent model # Registering the new RealEstateAgent model
@admin.register(RealEstateAgent) @admin.register(RealEstateAgent)
class RealEstateAgentAdmin(admin.ModelAdmin): class RealEstateAgentAdmin(admin.ModelAdmin):
list_display = ('user', 'brokerage_name', 'license_number', 'phone_number', 'city', 'state', 'agent_type', 'years_experience') list_display = (
list_filter = ('agent_type', 'state', 'years_experience', 'specialties') "user",
search_fields = ('user__email', 'user__first_name', 'user__last_name', 'brokerage_name', 'license_number', 'city') "brokerage_name",
#filter_horizontal = ('specialties', 'licensed_states') "license_number",
fieldsets = ( "phone_number",
(None, { "city",
'fields': ('user', 'brokerage_name', 'license_number', 'phone_number', 'address', 'city', 'state', 'zip_code', 'bio', 'profile_picture', 'website', 'agent_type') "state",
}), "agent_type",
('Professional Details', { "years_experience",
'fields': ('specialties', 'years_experience', 'licensed_states')
}),
('Location', {
'fields': ('latitude', 'longitude'),
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
) )
readonly_fields = ('created_at', 'updated_at') list_filter = ("agent_type", "state", "years_experience", "specialties")
search_fields = (
"user__email",
"user__first_name",
"user__last_name",
"brokerage_name",
"license_number",
"city",
)
# filter_horizontal = ('specialties', 'licensed_states')
fieldsets = (
(
None,
{
"fields": (
"user",
"brokerage_name",
"license_number",
"phone_number",
"address",
"city",
"state",
"zip_code",
"bio",
"profile_picture",
"website",
"agent_type",
)
},
),
(
"Professional Details",
{"fields": ("specialties", "years_experience", "licensed_states")},
),
(
"Location",
{
"fields": ("latitude", "longitude"),
},
),
(
"Timestamps",
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
),
)
readonly_fields = ("created_at", "updated_at")
@admin.register(UserViewModel) @admin.register(UserViewModel)
class UserViewModelAdmin(admin.ModelAdmin): class UserViewModelAdmin(admin.ModelAdmin):
list_display = ('pk','user','created_at') list_display = ("pk", "user", "created_at")
readonly_fields = ('created_at',) readonly_fields = ("created_at",)
@admin.register(PropertySave) @admin.register(PropertySave)
class PropertySaveAdmin(admin.ModelAdmin): class PropertySaveAdmin(admin.ModelAdmin):
list_display = ('pk', 'user','property') list_display = ("pk", "user", "property")
readonly_fields = ('created_at',) readonly_fields = ("created_at",)
@admin.register(Document)
class DocumentAdmin(admin.ModelAdmin):
list_display = ("document_type", "property", "uploaded_by")
@admin.register(SellerDisclosure)
class SellerDisclosureAdmin(admin.ModelAdmin):
model = SellerDisclosure
@admin.register(VendorPictures)
class VendorPicturesAdmin(admin.ModelAdmin):
list_display = ("category_name", "image")
model = VendorPictures
@admin.register(SupportAgent)
class SupportAgentAdmin(admin.ModelAdmin):
model = SupportAgent
@admin.register(FAQ)
class FAQAdmin(admin.ModelAdmin):
model = FAQ
@admin.register(SupportMessage)
class SupportMessageAdmin(admin.ModelAdmin):
model = SupportMessage
class SupportMessageStackedInline(admin.StackedInline):
model = SupportMessage
@admin.register(SupportCase)
class SupportCaseAdmin(admin.ModelAdmin):
model = SupportCase
inlines = [
SupportMessageStackedInline,
]
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(PropertyOwner, PropertyOwnerAdmin) admin.site.register(PropertyOwner, PropertyOwnerAdmin)
@@ -200,7 +336,7 @@ admin.site.register(VideoCategory, VideoCategoryAdmin)
admin.site.register(Video, VideoAdmin) admin.site.register(Video, VideoAdmin)
admin.site.register(UserVideoProgress, UserVideoProgressAdmin) admin.site.register(UserVideoProgress, UserVideoProgressAdmin)
admin.site.register(Conversation, ConversationAdmin) admin.site.register(Conversation, ConversationAdmin)
admin.site.register(Offer, OfferAdmin) admin.site.register(OfferDocument, OfferAdmin)
admin.site.register(PropertyPictures, PropertyPicturesAdmin) admin.site.register(PropertyPictures, PropertyPicturesAdmin)
admin.site.register(OpenHouse, OpenHouseAdmin) admin.site.register(OpenHouse, OpenHouseAdmin)

View File

@@ -1,5 +1,7 @@
import django_filters import django_filters
from .models import Property from .models import Property, Vendor
from django.db.models import QuerySet
from .utils import haversine_distance
class PropertyFilterSet(django_filters.FilterSet): class PropertyFilterSet(django_filters.FilterSet):
address = django_filters.CharFilter(field_name='address', lookup_expr='icontains') address = django_filters.CharFilter(field_name='address', lookup_expr='icontains')
@@ -17,3 +19,53 @@ class PropertyFilterSet(django_filters.FilterSet):
model = Property model = Property
fields = ['address', 'city', 'state', 'zip_code', 'min_num_bedrooms', 'max_num_bedrooms', fields = ['address', 'city', 'state', 'zip_code', 'min_num_bedrooms', 'max_num_bedrooms',
'min_num_bathrooms', 'max_num_bathrooms', 'min_sq_ft', 'max_sq_ft'] 'min_num_bathrooms', 'max_num_bathrooms', 'min_sq_ft', 'max_sq_ft']
class DistanceFilter(django_filters.Filter):
def filter(self, qs: QuerySet, value: str) -> QuerySet:
# The 'value' is expected to be a comma-separated string: "property_id,distance_in_miles"
if not value:
return qs
try:
property_id, distance_str = value.split(',')
property_id = int(property_id)
distance_miles = float(distance_str)
except (ValueError, IndexError):
return qs.none() # Return an empty queryset on malformed input
try:
# Import Property model here to avoid circular imports
from .models import Property
# Ensure the requesting user owns the property
request = self.parent.request
property_obj = Property.objects.get(id=property_id, owner__user=request.user)
if property_obj.latitude is None or property_obj.longitude is None:
return qs.none()
prop_lat = float(property_obj.latitude)
prop_lon = float(property_obj.longitude)
# This approach is database-agnostic but less performant
# than a database-native solution like PostGIS.
vendor_pks = []
for vendor in qs:
if vendor.latitude is not None and vendor.longitude is not None:
dist = haversine_distance(prop_lat, prop_lon, float(vendor.latitude), float(vendor.longitude))
if dist <= distance_miles:
vendor_pks.append(vendor.pk)
return qs.filter(pk__in=vendor_pks)
except Property.DoesNotExist:
return qs.none()
class VendorFilterSet(django_filters.FilterSet):
# Your existing filters
business_type = django_filters.ChoiceFilter(choices=Vendor.BUSINESS_TYPES)
# Custom distance filter
distance_from_property = DistanceFilter(label="property_id,distance_miles")
class Meta:
model = Vendor
fields = ['business_type', 'distance_from_property']

View File

@@ -0,0 +1,74 @@
# Generated by Django 5.2.4 on 2025-08-17 11:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0023_propertysave"),
]
operations = [
migrations.CreateModel(
name="Document",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("file", models.FileField(upload_to="property_documents/")),
(
"document_type",
models.CharField(
choices=[
("offer_letter", "Offer Letter"),
("attorney_contract", "Attorney Contract"),
("contractor_contract", "Contractor Contract"),
("title_report", "Title Report"),
("inspection_report", "Inspection Report"),
("deed", "Deed"),
("closing_disclosure", "Closing Disclosure"),
("other", "Other"),
],
max_length=50,
),
),
("description", models.TextField(blank=True, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"property",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="documents",
to="core.property",
),
),
(
"shared_with",
models.ManyToManyField(
blank=True,
related_name="shared_documents",
to=settings.AUTH_USER_MODEL,
),
),
(
"uploaded_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="uploaded_documents",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -0,0 +1,108 @@
# Generated by Django 5.2.4 on 2025-08-22 01:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0024_document"),
]
operations = [
migrations.CreateModel(
name="HomeImprovementReceipt",
fields=[
(
"document",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="home_improvement_receipt_data",
serialize=False,
to="core.document",
),
),
("vendor_id", models.IntegerField(blank=True, null=True)),
("date_of_work", models.DateField()),
("description", models.TextField()),
("cost", models.DecimalField(decimal_places=2, max_digits=10)),
],
),
migrations.CreateModel(
name="OfferDocument",
fields=[
(
"document",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="offer_data",
serialize=False,
to="core.document",
),
),
(
"offer_price",
models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
("closing_date", models.DateField(blank=True, null=True)),
("closing_days", models.IntegerField(blank=True, null=True)),
("contingencies", models.TextField(default="")),
],
),
migrations.CreateModel(
name="SellerDisclosure",
fields=[
(
"document",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="seller_disclosure_data",
serialize=False,
to="core.document",
),
),
("general_defects", models.TextField()),
("roof_condition", models.CharField(max_length=100)),
("roof_age", models.CharField(max_length=100)),
("known_roof_leaks", models.BooleanField()),
("plumbing_issues", models.TextField()),
("electrical_issues", models.TextField()),
("hvac_condition", models.CharField(max_length=100)),
("hvac_age", models.CharField(max_length=100)),
("known_lead_paint", models.BooleanField()),
("known_asbestos", models.BooleanField()),
("known_radon", models.BooleanField()),
("past_water_damage", models.TextField()),
("structural_issues", models.TextField()),
("neighborhood_nuisances", models.TextField()),
("property_line_disputes", models.TextField()),
("appliances_included", models.TextField()),
],
),
migrations.AlterField(
model_name="document",
name="document_type",
field=models.CharField(
choices=[
("offer_letter", "Offer Letter"),
("seller_disclosure", "Seller Disclosure"),
("home_improvement_receipt", "Home Improvement Receipt"),
("attorney_contract", "Attorney Contract"),
("contractor_contract", "Contractor Contract"),
("title_report", "Title Report"),
("inspection_report", "Inspection Report"),
("deed", "Deed"),
("closing_disclosure", "Closing Disclosure"),
("other", "Other"),
],
max_length=50,
),
),
migrations.DeleteModel(
name="Offer",
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.4 on 2025-08-22 01:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0025_homeimprovementreceipt_offerdocument_and_more"),
]
operations = [
migrations.AlterField(
model_name="document",
name="file",
field=models.FileField(
blank=True, null=True, upload_to="property_documents/"
),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.4 on 2025-09-09 02:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0026_alter_document_file"),
]
operations = [
migrations.AddField(
model_name="offerdocument",
name="status",
field=models.CharField(
choices=[
("submitted", "Submitted"),
("accepted", "Accepted"),
("rejected", "Rejected"),
("countered", "countered"),
("pending", "pending"),
],
default="submitted",
max_length=50,
),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.4 on 2025-09-10 17:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0027_offerdocument_status"),
]
operations = [
migrations.AddField(
model_name="offerdocument",
name="parent_offer",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="counter_offers",
to="core.offerdocument",
),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.2.4 on 2025-09-16 17:28
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0028_offerdocument_parent_offer"),
]
operations = [
migrations.AddField(
model_name="openhouse",
name="end_time",
field=models.TimeField(default=datetime.time(17, 0)),
),
migrations.AddField(
model_name="openhouse",
name="start_time",
field=models.TimeField(default=datetime.time(9, 0)),
),
migrations.AlterField(
model_name="openhouse",
name="updated_at",
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.2.4 on 2025-10-09 12:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0029_openhouse_end_time_openhouse_start_time_and_more"),
]
operations = [
migrations.CreateModel(
name="VendorPictures",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("image", models.FileField(upload_to="vendor_pictures/")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("category_name", models.CharField(max_length=100)),
],
),
migrations.AlterField(
model_name="propertypictures",
name="image",
field=models.FileField(upload_to="pictures/"),
),
]

View File

@@ -0,0 +1,57 @@
# Generated by Django 5.2.4 on 2025-10-09 15:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0030_vendorpictures_alter_propertypictures_image"),
]
operations = [
migrations.AlterField(
model_name="vendor",
name="business_type",
field=models.CharField(
choices=[
("arborist", "Arborist"),
(
"basement_waterproofing_and_injection",
"Basement Waterproofing And Injection",
),
("carpenter", "Carpenter"),
("cleaning Company", "Cleaning Company"),
("decking", "Decking"),
("door_company", "Door Company"),
("electrician", "Electrician"),
("fencing", "Fencing"),
("general_contractor", "General Contractor"),
("handyman", "Handyman"),
("home_inspector", "Home Inspector"),
("house_staging", "House Staging"),
("hvac", "HVAC"),
(
"irrigation_and_sprinkler_system",
"Irrigation And Sprinkler System",
),
("junk_removal", "Junk Removal"),
("landscaping", "Landscaping"),
("masonry", "Masonry"),
("mortgage_lendor", "Mortgage Lendor"),
("moving_company", "Moving Company"),
("painter", "Painter"),
("paving_company", "Paving Company"),
("pest_control", "Pest Control"),
("photographer", "Photographer"),
("plumber", "Plumber"),
("pressure_washing", "Pressure Washing"),
("roofer", "Roofer"),
("storage_facility", "Storage Facility"),
("window_company", "Window Company"),
("window_washing", "Window Washing"),
],
max_length=50,
),
),
]

View File

@@ -0,0 +1,140 @@
# Generated by Django 5.2.4 on 2025-10-13 15:27
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0031_alter_vendor_business_type"),
]
operations = [
migrations.CreateModel(
name="FAQ",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("question", models.TextField()),
("answer", models.TextField()),
("order", models.IntegerField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name="SupportAgent",
fields=[
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AlterField(
model_name="user",
name="user_type",
field=models.CharField(
choices=[
("property_owner", "Property Owner"),
("vendor", "Vendor"),
("attorney", "Attorney"),
("real_estate_agent", "Real Estate Agent"),
("support_agent", "Support Agent"),
("admin", "Admin"),
],
max_length=20,
),
),
migrations.CreateModel(
name="SupportCase",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.TextField()),
("description", models.TextField()),
(
"status",
models.CharField(
choices=[("opened", "Opened"), ("closed", "Closed")],
default="opened",
max_length=50,
),
),
(
"category",
models.CharField(
choices=[
("question", "Question"),
("bug", "Bug"),
("other", "Other"),
],
default="question",
max_length=50,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="SupportMessage",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("text", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"support_case",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="messages",
to="core.supportcase",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -11,6 +11,8 @@ from django.utils.html import strip_tags
from django.conf import settings from django.conf import settings
import uuid import uuid
import os import os
import datetime
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields): def create_user(self, email, password=None, **extra_fields):
@@ -35,6 +37,7 @@ class User(AbstractBaseUser, PermissionsMixin):
("vendor", "Vendor"), ("vendor", "Vendor"),
("attorney", "Attorney"), ("attorney", "Attorney"),
("real_estate_agent", "Real Estate Agent"), ("real_estate_agent", "Real Estate Agent"),
("support_agent", "Support Agent"),
("admin", "Admin"), ("admin", "Admin"),
) )
USER_TIER_CHOICES = ( USER_TIER_CHOICES = (
@@ -83,17 +86,43 @@ class PropertyOwner(models.Model):
class Vendor(models.Model): class Vendor(models.Model):
BUSINESS_TYPES = ( BUSINESS_TYPES = (
("electrician", "Electrician"), ("arborist", "Arborist"),
(
"basement_waterproofing_and_injection",
"Basement Waterproofing And Injection",
),
("carpenter", "Carpenter"), ("carpenter", "Carpenter"),
("cleaning Company", "Cleaning Company"),
("decking", "Decking"),
("door_company", "Door Company"),
("electrician", "Electrician"),
("fencing", "Fencing"),
("general_contractor", "General Contractor"),
("handyman", "Handyman"),
("home_inspector", "Home Inspector"),
("house_staging", "House Staging"),
("hvac", "HVAC"),
("irrigation_and_sprinkler_system", "Irrigation And Sprinkler System"),
("junk_removal", "Junk Removal"),
("landscaping", "Landscaping"),
("masonry", "Masonry"),
("mortgage_lendor", "Mortgage Lendor"),
("moving_company", "Moving Company"),
("painter", "Painter"),
("paving_company", "Paving Company"),
("pest_control", "Pest Control"),
("photographer", "Photographer"),
("plumber", "Plumber"), ("plumber", "Plumber"),
("inspector", "Inspector"), ("pressure_washing", "Pressure Washing"),
("lender", "Lender"), ("roofer", "Roofer"),
("other", "Other"), ("storage_facility", "Storage Facility"),
("window_company", "Window Company"),
("window_washing", "Window Washing"),
) )
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
business_name = models.CharField(max_length=100) business_name = models.CharField(max_length=100)
business_type = models.CharField(max_length=20, choices=BUSINESS_TYPES) business_type = models.CharField(max_length=50, choices=BUSINESS_TYPES)
phone_number = models.CharField(max_length=20, blank=True, null=True) phone_number = models.CharField(max_length=20, blank=True, null=True)
address = models.CharField(max_length=200) address = models.CharField(max_length=200)
city = models.CharField(max_length=100) city = models.CharField(max_length=100)
@@ -123,25 +152,27 @@ class Vendor(models.Model):
) # For coordinates ) # For coordinates
views = models.IntegerField(default=0) views = models.IntegerField(default=0)
def __str__(self): def __str__(self):
return self.business_name return self.business_name
class Attorney(models.Model): class Attorney(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
firm_name = models.CharField(max_length=200) firm_name = models.CharField(max_length=200)
bar_number = models.CharField(max_length=50, unique=True, blank=True, null=True) # Bar numbers are typically unique bar_number = models.CharField(
max_length=50, unique=True, blank=True, null=True
) # Bar numbers are typically unique
phone_number = models.CharField(max_length=20, blank=True, null=True) phone_number = models.CharField(max_length=20, blank=True, null=True)
address = models.CharField(max_length=200) address = models.CharField(max_length=200)
city = models.CharField(max_length=100) city = models.CharField(max_length=100)
state = models.CharField(max_length=2) state = models.CharField(max_length=2)
zip_code = models.CharField(max_length=10) zip_code = models.CharField(max_length=10)
specialties = models.JSONField(blank=True, default=list) # Store as JSON array specialties = models.JSONField(blank=True, default=list) # Store as JSON array
years_experience = models.IntegerField(default=0) years_experience = models.IntegerField(default=0)
website = models.URLField(blank=True, null=True) website = models.URLField(blank=True, null=True)
profile_picture = models.URLField(max_length=500, blank=True, null=True) profile_picture = models.URLField(max_length=500, blank=True, null=True)
bio = models.TextField(blank=True, null=True) # Use TextField for longer text bio = models.TextField(blank=True, null=True) # Use TextField for longer text
licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -155,6 +186,7 @@ class Attorney(models.Model):
def __str__(self): def __str__(self):
return f"{self.user.get_full_name()} ({self.firm_name})" return f"{self.user.get_full_name()} ({self.firm_name})"
class RealEstateAgent(models.Model): class RealEstateAgent(models.Model):
AGENT_TYPE_CHOICES = ( AGENT_TYPE_CHOICES = (
("buyer_agent", "Buyer's Agent"), ("buyer_agent", "Buyer's Agent"),
@@ -165,19 +197,23 @@ class RealEstateAgent(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
brokerage_name = models.CharField(max_length=200) brokerage_name = models.CharField(max_length=200)
license_number = models.CharField(max_length=50, unique=True) # License numbers are typically unique license_number = models.CharField(
max_length=50, unique=True
) # License numbers are typically unique
phone_number = models.CharField(max_length=20, blank=True, null=True) phone_number = models.CharField(max_length=20, blank=True, null=True)
address = models.CharField(max_length=200) address = models.CharField(max_length=200)
city = models.CharField(max_length=100) city = models.CharField(max_length=100)
state = models.CharField(max_length=2) state = models.CharField(max_length=2)
zip_code = models.CharField(max_length=10) zip_code = models.CharField(max_length=10)
specialties = models.JSONField(blank=True, default=list) # Store as JSON array specialties = models.JSONField(blank=True, default=list) # Store as JSON array
years_experience = models.IntegerField(default=0) years_experience = models.IntegerField(default=0)
website = models.URLField(blank=True, null=True) website = models.URLField(blank=True, null=True)
profile_picture = models.URLField(max_length=500, blank=True, null=True) profile_picture = models.URLField(max_length=500, blank=True, null=True)
bio = models.TextField(blank=True, null=True) bio = models.TextField(blank=True, null=True)
licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array
agent_type = models.CharField(max_length=20, choices=AGENT_TYPE_CHOICES, default="other") agent_type = models.CharField(
max_length=20, choices=AGENT_TYPE_CHOICES, default="other"
)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -192,12 +228,15 @@ class RealEstateAgent(models.Model):
return f"{self.user.get_full_name()} ({self.brokerage_name})" return f"{self.user.get_full_name()} ({self.brokerage_name})"
class SupportAgent(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
class UserViewModel(models.Model): class UserViewModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
class Property(models.Model): class Property(models.Model):
PROPERTY_STATUS_TYPES = ( PROPERTY_STATUS_TYPES = (
("active", "Active"), ("active", "Active"),
@@ -250,16 +289,22 @@ class Property(models.Model):
saves = models.IntegerField(default=0) saves = models.IntegerField(default=0)
listed_date = models.DateTimeField(blank=True, null=True) listed_date = models.DateTimeField(blank=True, null=True)
def __str__(self): def __str__(self):
return f"{self.address}, {self.city}, {self.state} {self.zip_code}" return f"{self.address}, {self.city}, {self.state} {self.zip_code}"
class SchoolInfo(models.Model): class SchoolInfo(models.Model):
SCHOOL_TYPES = ( SCHOOL_TYPES = (
("Public", "Public"), ("Public", "Public"),
("Other", "Other"), ("Other", "Other"),
) )
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name="schools", blank=True, null=True) property = models.ForeignKey(
Property,
on_delete=models.CASCADE,
related_name="schools",
blank=True,
null=True,
)
city = models.CharField(max_length=100) city = models.CharField(max_length=100)
state = models.CharField(max_length=2) state = models.CharField(max_length=2)
zip_code = models.CharField(max_length=10) zip_code = models.CharField(max_length=10)
@@ -280,8 +325,11 @@ class SchoolInfo(models.Model):
max_length=15, choices=SCHOOL_TYPES, default="public" max_length=15, choices=SCHOOL_TYPES, default="public"
) )
class PropertyTaxInfo(models.Model): class PropertyTaxInfo(models.Model):
property = models.OneToOneField(Property, on_delete=models.CASCADE, related_name="tax_info") property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="tax_info"
)
assessed_value = models.IntegerField() assessed_value = models.IntegerField()
assessment_year = models.IntegerField() assessment_year = models.IntegerField()
tax_amount = models.FloatField() tax_amount = models.FloatField()
@@ -291,7 +339,9 @@ class PropertyTaxInfo(models.Model):
class PropertySaleInfo(models.Model): class PropertySaleInfo(models.Model):
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name="sale_info") property = models.ForeignKey(
Property, on_delete=models.CASCADE, related_name="sale_info"
)
seq_no = models.IntegerField() seq_no = models.IntegerField()
sale_date = models.DateTimeField() sale_date = models.DateTimeField()
sale_amount = models.FloatField() sale_amount = models.FloatField()
@@ -301,7 +351,11 @@ class PropertySaleInfo(models.Model):
class PropertyWalkScoreInfo(models.Model): class PropertyWalkScoreInfo(models.Model):
property = models.OneToOneField( property = models.OneToOneField(
Property, on_delete=models.CASCADE, blank=True, null=True, related_name="walk_score" Property,
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="walk_score",
) )
walk_score = models.IntegerField() walk_score = models.IntegerField()
walk_description = models.CharField(max_length=256) walk_description = models.CharField(max_length=256)
@@ -317,21 +371,36 @@ class PropertyWalkScoreInfo(models.Model):
class OpenHouse(models.Model): class OpenHouse(models.Model):
property = models.ForeignKey(Property, on_delete=models.CASCADE, blank=True, null=True, related_name="open_houses") property = models.ForeignKey(
Property,
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="open_houses",
)
listed_date = models.DateTimeField() listed_date = models.DateTimeField()
start_time = models.TimeField(default=datetime.time(9, 0))
end_time = models.TimeField(default=datetime.time(17, 0))
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now_add=True)
class PropertyPictures(models.Model): class PropertyPictures(models.Model):
Property = models.ForeignKey( Property = models.ForeignKey(
Property, on_delete=models.CASCADE, related_name="pictures" Property, on_delete=models.CASCADE, related_name="pictures"
) )
image = models.FileField(upload_to="pcitures/") image = models.FileField(upload_to="pictures/")
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
class VendorPictures(models.Model):
image = models.FileField(upload_to="vendor_pictures/")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
category_name = models.CharField(max_length=100)
class VideoCategory(models.Model): class VideoCategory(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
@@ -472,29 +541,30 @@ class PasswordResetToken(models.Model):
return f"Password reset token for {self.user.email}" return f"Password reset token for {self.user.email}"
class Offer(models.Model): # class Offer(models.Model):
OFFER_STATUS_TYPES = ( # OFFER_STATUS_TYPES = (
("submitted", "Submitted"), # ("submitted", "Submitted"),
("draft", "Draft"), # ("draft", "Draft"),
("accepted", "Accepted"), # ("accepted", "Accepted"),
("rejected", "Rejected"), # ("rejected", "Rejected"),
("counter", "Counter"), # ("counter", "Counter"),
("withdrawn", "Withdrawn"), # ("withdrawn", "Withdrawn"),
) # )
user = models.ForeignKey(User, on_delete=models.CASCADE) # user = models.ForeignKey(User, on_delete=models.CASCADE)
property = models.ForeignKey(Property, on_delete=models.PROTECT) # property = models.ForeignKey(Property, on_delete=models.PROTECT)
status = models.CharField( # status = models.CharField(
max_length=10, choices=OFFER_STATUS_TYPES, default="draft" # max_length=10, choices=OFFER_STATUS_TYPES, default="draft"
) # )
previous_offer = models.ForeignKey( # previous_offer = models.ForeignKey(
"self", on_delete=models.CASCADE, null=True, blank=True # "self", on_delete=models.CASCADE, null=True, blank=True
) # )
is_active = models.BooleanField(default=True) # is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True) # created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) # updated_at = models.DateTimeField(auto_now=True)
# def __str__(self):
# return f"{self.user.email} {self.status} {self.property.address}"
def __str__(self):
return f"{self.user.email} {self.status} {self.property.address}"
class Bid(models.Model): class Bid(models.Model):
BID_TYPE_CHOICES = ( BID_TYPE_CHOICES = (
@@ -512,7 +582,9 @@ class Bid(models.Model):
("outside", "Outside"), ("outside", "Outside"),
) )
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name="bids") property = models.ForeignKey(
Property, on_delete=models.CASCADE, related_name="bids"
)
description = models.TextField() description = models.TextField()
bid_type = models.CharField(max_length=50, choices=BID_TYPE_CHOICES) bid_type = models.CharField(max_length=50, choices=BID_TYPE_CHOICES)
location = models.CharField(max_length=50, choices=LOCATION_CHOICES) location = models.CharField(max_length=50, choices=LOCATION_CHOICES)
@@ -522,11 +594,13 @@ class Bid(models.Model):
def __str__(self): def __str__(self):
return f"Bid for {self.bid_type} at {self.property.address}" return f"Bid for {self.bid_type} at {self.property.address}"
class BidImage(models.Model): class BidImage(models.Model):
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="images") bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="images")
image = models.FileField(upload_to="bid_pictures/") image = models.FileField(upload_to="bid_pictures/")
uploaded_at = models.DateTimeField(auto_now_add=True) uploaded_at = models.DateTimeField(auto_now_add=True)
class BidResponse(models.Model): class BidResponse(models.Model):
RESPONSE_STATUS_CHOICES = ( RESPONSE_STATUS_CHOICES = (
("draft", "Draft"), ("draft", "Draft"),
@@ -534,23 +608,190 @@ class BidResponse(models.Model):
("selected", "Selected"), ("selected", "Selected"),
) )
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="responses") bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="responses")
vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name="bid_responses") vendor = models.ForeignKey(
Vendor, on_delete=models.CASCADE, related_name="bid_responses"
)
description = models.TextField() description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2) price = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=RESPONSE_STATUS_CHOICES, default="draft") status = models.CharField(
max_length=20, choices=RESPONSE_STATUS_CHOICES, default="draft"
)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
class Meta: class Meta:
unique_together = ('bid', 'vendor') unique_together = ("bid", "vendor")
def __str__(self): def __str__(self):
return f"Response from {self.vendor.business_name} for Bid {self.bid.id}" return f"Response from {self.vendor.business_name} for Bid {self.bid.id}"
class PropertySave(models.Model): class PropertySave(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
property = models.ForeignKey(Property, on_delete=models.CASCADE) property = models.ForeignKey(Property, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
class Meta: class Meta:
unique_together = ('user', 'property') unique_together = ("user", "property")
class Document(models.Model):
DOCUMENT_TYPES = (
("offer_letter", "Offer Letter"),
("seller_disclosure", "Seller Disclosure"),
("home_improvement_receipt", "Home Improvement Receipt"),
("attorney_contract", "Attorney Contract"),
("contractor_contract", "Contractor Contract"),
("title_report", "Title Report"),
("inspection_report", "Inspection Report"),
("deed", "Deed"),
("closing_disclosure", "Closing Disclosure"),
("other", "Other"),
)
# Link to the property the document belongs to
property = models.ForeignKey(
Property, on_delete=models.CASCADE, related_name="documents"
)
# The document file itself
file = models.FileField(upload_to="property_documents/", blank=True, null=True)
# The type of document
document_type = models.CharField(max_length=50, choices=DOCUMENT_TYPES)
# Optional description of the document
description = models.TextField(blank=True, null=True)
# The user who uploaded the document
uploaded_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="uploaded_documents"
)
# A list of users who have permission to view this document
shared_with = models.ManyToManyField(
User, related_name="shared_documents", blank=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.document_type} for {self.property.address}"
class OfferDocument(models.Model):
OFFER_STATUS = (
("submitted", "Submitted"),
("accepted", "Accepted"),
("rejected", "Rejected"),
("countered", "countered"),
("pending", "pending"),
)
# One-to-one link to the generic Document
document = models.OneToOneField(
Document, on_delete=models.CASCADE, related_name="offer_data", primary_key=True
)
parent_offer = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="counter_offers",
)
offer_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
closing_date = models.DateField(null=True, blank=True)
closing_days = models.IntegerField(null=True, blank=True)
contingencies = models.TextField(default="")
status = models.CharField(max_length=50, choices=OFFER_STATUS, default="submitted")
def __str__(self):
return f"Offer for Document ID: {self.document.id}"
class SellerDisclosure(models.Model):
# One-to-one link to the generic Document
document = models.OneToOneField(
Document,
on_delete=models.CASCADE,
related_name="seller_disclosure_data",
primary_key=True,
)
general_defects = models.TextField()
roof_condition = models.CharField(max_length=100)
roof_age = models.CharField(max_length=100)
known_roof_leaks = models.BooleanField()
plumbing_issues = models.TextField()
electrical_issues = models.TextField()
hvac_condition = models.CharField(max_length=100)
hvac_age = models.CharField(max_length=100)
known_lead_paint = models.BooleanField()
known_asbestos = models.BooleanField()
known_radon = models.BooleanField()
past_water_damage = models.TextField()
structural_issues = models.TextField()
neighborhood_nuisances = models.TextField()
property_line_disputes = models.TextField()
appliances_included = models.TextField()
def __str__(self):
return f"Seller Disclosure for Document ID: {self.document.id}"
class HomeImprovementReceipt(models.Model):
# One-to-one link to the generic Document
document = models.OneToOneField(
Document,
on_delete=models.CASCADE,
related_name="home_improvement_receipt_data",
primary_key=True,
)
# Assuming vendor_id is just an integer, not a ForeignKey to a Vendor model for now
vendor_id = models.IntegerField(null=True, blank=True)
date_of_work = models.DateField()
description = models.TextField()
cost = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"Home Improvement Receipt for Document ID: {self.document.id}"
class FAQ(models.Model):
question = models.TextField()
answer = models.TextField()
order = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SupportCase(models.Model):
SUPPORT_STATUS = (
("opened", "Opened"),
("closed", "Closed"),
)
SUPPORT_CATEGORIES = (
("question", "Question"),
("bug", "Bug"),
("other", "Other"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.TextField()
description = models.TextField()
status = models.CharField(max_length=50, choices=SUPPORT_STATUS, default="opened")
category = models.CharField(
max_length=50, choices=SUPPORT_CATEGORIES, default="question"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SupportMessage(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
support_case = models.ForeignKey(
SupportCase, on_delete=models.CASCADE, related_name="messages"
)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

View File

@@ -3,7 +3,6 @@ from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission): class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS: if request.method in permissions.SAFE_METHODS:
return True return True
@@ -27,6 +26,16 @@ class IsPropertyOwner(permissions.BasePermission):
return False return False
class IsSupportAgent(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.user_type == "support_agent"
def has_object_permission(self, request, view, obj):
if hasattr(obj, "support_agent"):
return obj.support_agent.user == request.user
return False
class IsVendor(permissions.BasePermission): class IsVendor(permissions.BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user.user_type == "vendor" return request.user.user_type == "vendor"
@@ -58,6 +67,38 @@ class IsParticipantInOffer(permissions.BasePermission):
There are two options, either you are the sender or the owner of the property There are two options, either you are the sender or the owner of the property
""" """
if hasattr(obj, "user") and hasattr(obj, "property"): if hasattr(obj, "user") and hasattr(obj, "property"):
return request.user == obj.user or request.user == obj.property.owner.user return request.user == obj.user or request.user == obj.property.owner.user
return False return False
class IsPropertyOwnerOrVendorOrAttorney(permissions.BasePermission):
"""
Custom permission to only allow property owners, vendors, or attorneys
who are part of the property transaction to view or edit it.
"""
def has_object_permission(self, request, view, obj):
user = request.user
property_owner = obj.property.owner.user
# Check if the user is the property owner
if user == property_owner:
return True
# Check if the user is a vendor or attorney associated with a bid/bid response
if user.user_type in ["vendor", "attorney"]:
# Check for vendor-related bids for this property
is_vendor_for_property = obj.property.bids.filter(
responses__vendor__user=user
).exists()
# For attorneys, you would need a new model linking attorneys to properties.
# Assuming a simpler check for now.
is_attorney_for_property = (
False # Implement this based on your attorney model relationships
)
if is_vendor_for_property or is_attorney_for_property:
return True
return False

View File

@@ -0,0 +1,107 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from django.http import Http404
from rest_framework.permissions import AllowAny
from core.models import Property
from core.serializers import PublicPropertyResponseSerializer as PropertySerializer
class PropertyList(APIView):
"""
List all active properties.
"""
permission_classes = [
AllowAny,
]
def get(self, request, format=None):
properties = Property.objects.filter(property_status="active")
address = request.query_params.get("address")
if address:
properties = properties.filter(address__icontains=address)
city = request.query_params.get("city")
if city:
properties = properties.filter(city__icontains=city)
state = request.query_params.get("state")
if state:
properties = properties.filter(state__iexact=state)
zip_code = request.query_params.get("zipCode")
if zip_code:
properties = properties.filter(zip_code=zip_code)
min_sq_ft = request.query_params.get("minSqFt")
if min_sq_ft:
properties = properties.filter(sq_ft__gte=min_sq_ft)
max_sq_ft = request.query_params.get("maxSqFt")
if max_sq_ft:
properties = properties.filter(sq_ft__lte=max_sq_ft)
min_bedrooms = request.query_params.get("minBedrooms")
if min_bedrooms:
properties = properties.filter(num_bedrooms__gte=min_bedrooms)
max_bedrooms = request.query_params.get("maxBedrooms")
if max_bedrooms:
properties = properties.filter(num_bedrooms__lte=max_bedrooms)
min_bathrooms = request.query_params.get("minBathrooms")
if min_bathrooms:
properties = properties.filter(num_bathrooms__gte=min_bathrooms)
max_bathrooms = request.query_params.get("maxBathrooms")
if max_bathrooms:
properties = properties.filter(num_bathrooms__lte=max_bathrooms)
serializer = PropertySerializer(properties, many=True)
return Response(serializer.data)
class PropertyDetail(APIView):
"""
Retrieve a single property.
"""
permission_classes = [
AllowAny,
]
def get_object(self, pk):
print("asdlkfjhaslkdjhfalksjdhf")
try:
return Property.objects.get(id=pk, property_status="active")
except Property.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
try:
property = self.get_object(pk)
serializer = PropertySerializer(property)
return Response(serializer.data)
except Exception as e:
print(e)
return Response(status=400)
class PropertyIncrementCount(APIView):
permission_classes = [
AllowAny,
]
def get_object(self, pk):
try:
return Property.objects.get(id=pk, property_status="active")
except Property.DoesNotExist:
raise Http404
def post(self, request, pk, format=None):
property = self.get_object(pk)
property.views += 1
property.save()
serializer = PropertySerializer(property)
return Response(serializer.data)

View File

@@ -1,9 +1,11 @@
from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from .models import ( from .models import (
PropertyOwner, PropertyOwner,
SupportAgent,
Vendor, Vendor,
Property, Property,
VideoCategory, VideoCategory,
@@ -12,14 +14,26 @@ from .models import (
Conversation, Conversation,
Message, Message,
PasswordResetToken, PasswordResetToken,
Offer, OfferDocument,
PropertyPictures, PropertyPictures,
OpenHouse, OpenHouse,
PropertySaleInfo, PropertySaleInfo,
PropertyTaxInfo, PropertyTaxInfo,
PropertyWalkScoreInfo, PropertyWalkScoreInfo,
SchoolInfo, SchoolInfo,
Bid, BidImage, BidResponse, RealEstateAgent, Attorney, UserViewModel, PropertySave Bid,
BidImage,
BidResponse,
RealEstateAgent,
Attorney,
UserViewModel,
PropertySave,
Document,
SellerDisclosure,
HomeImprovementReceipt,
SupportCase,
SupportMessage,
FAQ,
) )
from django.core.mail import send_mail from django.core.mail import send_mail
from django.template.loader import render_to_string from django.template.loader import render_to_string
@@ -143,6 +157,7 @@ class PropertyOwnerSerializer(serializers.ModelSerializer):
class VendorSerializer(serializers.ModelSerializer): class VendorSerializer(serializers.ModelSerializer):
user = UserSerializer() user = UserSerializer()
class Meta: class Meta:
model = Vendor model = Vendor
fields = [ fields = [
@@ -163,7 +178,7 @@ class VendorSerializer(serializers.ModelSerializer):
"latitude", "latitude",
"profile_picture", "profile_picture",
"user", "user",
"views" "views",
] ]
read_only_fields = [ read_only_fields = [
"id", "id",
@@ -172,10 +187,13 @@ class VendorSerializer(serializers.ModelSerializer):
"average_rating", "average_rating",
"num_reviews", "num_reviews",
] ]
# This create method is fine for creating a new vendor and user # This create method is fine for creating a new vendor and user
def create(self, validated_data): def create(self, validated_data):
user_data = validated_data.pop("user") user_data = validated_data.pop("user")
user = User.objects.create_user(**user_data) # Use create_user to hash the password if present user = User.objects.create_user(
**user_data
) # Use create_user to hash the password if present
vendor = Vendor.objects.create(user=user, **validated_data) vendor = Vendor.objects.create(user=user, **validated_data)
return vendor return vendor
@@ -219,7 +237,15 @@ class PropertyPictureSerializer(serializers.ModelSerializer):
class OpenHouseSerializer(serializers.ModelSerializer): class OpenHouseSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = OpenHouse model = OpenHouse
fields = ["id", "created_at", "updated_at", "listed_date", "property"] fields = [
"id",
"created_at",
"updated_at",
"listed_date",
"property",
"start_time",
"end_time",
]
read_only_fields = ["id", "created_at", "updated_at"] read_only_fields = ["id", "created_at", "updated_at"]
def create(self, validated_data): def create(self, validated_data):
@@ -234,6 +260,49 @@ class OpenHouseSerializer(serializers.ModelSerializer):
return super().create(validated_data) return super().create(validated_data)
class SubDocumentField(serializers.RelatedField):
def to_representation(self, value):
from .serializers import (
OfferSerializer,
SellerDisclosureSerializer,
HomeImprovementReceiptSerializer,
)
# Check for each related type and use the correct serializer
if hasattr(value, "offer_data"):
return OfferSerializer(value.offer_data).data
elif hasattr(value, "seller_disclosure_data"):
return SellerDisclosureSerializer(value.seller_disclosure_data).data
elif hasattr(value, "home_improvement_receipt_data"):
return HomeImprovementReceiptSerializer(
value.home_improvement_receipt_data
).data
return None
class DocumentSerializer(serializers.ModelSerializer):
sub_document = SubDocumentField(source="*", read_only=True)
shared_with = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), many=True, required=False
) # Allow sharing
class Meta:
model = Document
fields = [
"id",
"property",
"file",
"document_type",
"description",
"uploaded_by",
"shared_with",
"created_at",
"updated_at",
"sub_document",
]
read_only_fields = ["id", "created_at", "updated_at"]
class SchoolInfoSerializer(serializers.ModelSerializer): class SchoolInfoSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SchoolInfo model = SchoolInfo
@@ -306,13 +375,59 @@ class PropertySaleInfoSerializer(serializers.ModelSerializer):
"sale_amount", "sale_amount",
"created_at", "created_at",
"updated_at", "updated_at",
] ]
read_only_fields = ["id", "created_at", "updated_at"] read_only_fields = ["id", "created_at", "updated_at"]
class PublicPropertyResponseSerializer(serializers.ModelSerializer):
pictures = PropertyPictureSerializer(many=True)
open_houses = OpenHouseSerializer(many=True)
schools = SchoolInfoSerializer(many=True)
walk_score = PropertyWalkScoreInfoSerializer(many=False)
tax_info = PropertyTaxInfoSerializer(many=False)
sale_info = PropertySaleInfoSerializer(many=True)
class Meta:
model = Property
fields = [
"id",
"address",
"street",
"city",
"state",
"zip_code",
"market_value",
"loan_amount",
"loan_interest_rate",
"loan_term",
"loan_start_date",
"created_at",
"updated_at",
"description",
"sq_ft",
"features",
"num_bedrooms",
"num_bathrooms",
"latitude",
"longitude",
"realestate_api_id",
"property_status",
"views",
"saves",
"listed_date",
"pictures",
"open_houses",
"schools",
"walk_score",
"tax_info",
"sale_info",
]
read_only_fields = ["id", "created_at", "updated_at", "documents"]
class PropertyResponseSerializer(serializers.ModelSerializer): class PropertyResponseSerializer(serializers.ModelSerializer):
owner = PropertyOwnerSerializer() owner = PropertyOwnerSerializer()
documents = DocumentSerializer(many=True, read_only=True)
pictures = PropertyPictureSerializer(many=True) pictures = PropertyPictureSerializer(many=True)
open_houses = OpenHouseSerializer(many=True) open_houses = OpenHouseSerializer(many=True)
schools = SchoolInfoSerializer(many=True) schools = SchoolInfoSerializer(many=True)
@@ -350,13 +465,14 @@ class PropertyResponseSerializer(serializers.ModelSerializer):
"saves", "saves",
"listed_date", "listed_date",
"pictures", "pictures",
'open_houses', "open_houses",
'schools', "schools",
'walk_score', "walk_score",
'tax_info', "tax_info",
'sale_info', "sale_info",
"documents",
] ]
read_only_fields = ["id", "created_at", "updated_at"] read_only_fields = ["id", "created_at", "updated_at", "documents"]
class PropertyRequestSerializer(serializers.ModelSerializer): class PropertyRequestSerializer(serializers.ModelSerializer):
@@ -395,10 +511,10 @@ class PropertyRequestSerializer(serializers.ModelSerializer):
"listed_date", "listed_date",
"tax_info", "tax_info",
"sale_info", "sale_info",
"schools" "schools",
] ]
read_only_fields = ["id", "created_at", "updated_at", "views", "saves"] read_only_fields = ["id", "created_at", "updated_at", "views", "saves"]
def create(self, validated_data): def create(self, validated_data):
# tax_info_data = validated_data.pop("tax_info") # tax_info_data = validated_data.pop("tax_info")
# tax_info_data = validated_data.pop("tax_info") # tax_info_data = validated_data.pop("tax_info")
@@ -408,17 +524,18 @@ class PropertyRequestSerializer(serializers.ModelSerializer):
sale_info = validated_data.pop("sale_info") sale_info = validated_data.pop("sale_info")
schools = [] schools = []
property_instance = Property.objects.create(**validated_data) property_instance = Property.objects.create(**validated_data)
sale_infos = [] sale_infos = []
for sale_in in sale_info: for sale_in in sale_info:
sale_infos.append(PropertySaleInfo.objects.create(**sale_in, property=property_instance)) sale_infos.append(
PropertySaleInfo.objects.create(**sale_in, property=property_instance)
)
for school_data in schools_data: for school_data in schools_data:
schools.append(SchoolInfo.objects.create(**school_data, property=property_instance)) schools.append(
SchoolInfo.objects.create(**school_data, property=property_instance)
)
PropertyTaxInfo.objects.create(**tax_info, property=property_instance) PropertyTaxInfo.objects.create(**tax_info, property=property_instance)
walk_score.property = property_instance walk_score.property = property_instance
@@ -426,6 +543,54 @@ class PropertyRequestSerializer(serializers.ModelSerializer):
return property_instance return property_instance
def update(self, instance, validated_data):
# New logic for updating listed_date based on property_status
new_status = validated_data.get("property_status", None)
if new_status and new_status != instance.property_status:
# Status is changing
if new_status == "active":
# Set listed_date to current time
validated_data["listed_date"] = timezone.now()
elif new_status == "off_market":
# Set listed_date to null
validated_data["listed_date"] = None
# Handle nested updates
schools_data = validated_data.pop("schools", None)
tax_info_data = validated_data.pop("tax_info", None)
sale_info_data = validated_data.pop("sale_info", None)
walk_score_data = validated_data.pop("walk_score", None)
# Update the main Property instance
instance = super().update(instance, validated_data)
# Handle nested updates (e.g., update or create new nested objects)
if tax_info_data:
tax_info_instance, created = PropertyTaxInfo.objects.update_or_create(
property=instance, defaults=tax_info_data
)
if walk_score_data:
walk_score_instance, created = WalkScore.objects.update_or_create(
property=instance, defaults=walk_score_data
)
# For "many" relationships like schools and sale_info, you might need more complex logic
# (e.g., clearing old objects and creating new ones, or matching by ID)
if schools_data is not None:
# Example: Clear existing schools and create new ones
instance.schools.all().delete()
for school_data in schools_data:
SchoolInfo.objects.create(property=instance, **school_data)
if sale_info_data is not None:
instance.sale_info.all().delete()
for sale_data in sale_info_data:
PropertySaleInfo.objects.create(property=instance, **sale_data)
return instance
class VideoCategorySerializer(serializers.ModelSerializer): class VideoCategorySerializer(serializers.ModelSerializer):
class Meta: class Meta:
@@ -518,7 +683,6 @@ class UserVideoProgressSerializer(serializers.ModelSerializer):
class MessageSerializer(serializers.ModelSerializer): class MessageSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Message model = Message
fields = [ fields = [
@@ -622,7 +786,6 @@ class PasswordResetConfirmSerializer(serializers.Serializer):
new_password2 = serializers.CharField(write_only=True) new_password2 = serializers.CharField(write_only=True)
def validate(self, attrs): def validate(self, attrs):
try: try:
token = PasswordResetToken.objects.get(token=attrs["token"]) token = PasswordResetToken.objects.get(token=attrs["token"])
except PasswordResetToken.DoesNotExist: except PasswordResetToken.DoesNotExist:
@@ -650,99 +813,138 @@ class PasswordResetConfirmSerializer(serializers.Serializer):
return user return user
class OfferRequestSerializer(serializers.ModelSerializer): # class OfferRequestSerializer(serializers.ModelSerializer):
previous_offer = serializers.PrimaryKeyRelatedField(read_only=True) # previous_offer = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta: # class Meta:
model = Offer # model = Offer
fields = ["id", "user", "property", "status", "previous_offer", "is_active"] # fields = ["id", "user", "property", "status", "previous_offer", "is_active"]
read_only_fields = ["id", "created_at", "updated_at"] # read_only_fields = ["id", "created_at", "updated_at"]
def get_previous_offer(self, model_field): # def get_previous_offer(self, model_field):
return OfferRequestSerializer() # return OfferRequestSerializer()
class OfferResponseSerializer(serializers.ModelSerializer): # class OfferResponseSerializer(serializers.ModelSerializer):
previous_offer = serializers.PrimaryKeyRelatedField(read_only=True) # previous_offer = serializers.PrimaryKeyRelatedField(read_only=True)
user = UserSerializer() # user = UserSerializer()
property = PropertyResponseSerializer() # property = PropertyResponseSerializer()
class Meta: # class Meta:
model = Offer # model = Offer
fields = [ # fields = [
"id", # "id",
"user", # "user",
"property", # "property",
"status", # "status",
"previous_offer", # "previous_offer",
"is_active", # "is_active",
"created_at", # "created_at",
"updated_at", # "updated_at",
] # ]
read_only_fields = ["id", "created_at", "updated_at"] # read_only_fields = ["id", "created_at", "updated_at"]
def get_previous_offer(self, model_field): # def get_previous_offer(self, model_field):
return OfferResponseSerializer() # return OfferResponseSerializer()
# def validate_status(self, value):
# return value
def validate_status(self, value):
return value
class BidImageSerializer(serializers.ModelSerializer): class BidImageSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = BidImage model = BidImage
fields = ["id", "image"] fields = ["id", "image"]
class BidResponseSerializer(serializers.ModelSerializer): class BidResponseSerializer(serializers.ModelSerializer):
vendor = VendorSerializer(read_only=True) vendor = VendorSerializer(read_only=True)
class Meta: class Meta:
model = BidResponse model = BidResponse
fields = ["id", "bid", "vendor", "description", "price", "status", "created_at", "updated_at"] fields = [
"id",
"bid",
"vendor",
"description",
"price",
"status",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at", "vendor"] read_only_fields = ["id", "created_at", "updated_at", "vendor"]
class BidSerializer(serializers.ModelSerializer): class BidSerializer(serializers.ModelSerializer):
images = BidImageSerializer(many=True, read_only=True) images = BidImageSerializer(many=True, read_only=True)
responses = BidResponseSerializer(many=True, read_only=True) responses = BidResponseSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Bid model = Bid
fields = ["id", "property", "description", "bid_type", "location", "created_at", "updated_at", "images", "responses"] fields = [
"id",
"property",
"description",
"bid_type",
"location",
"created_at",
"updated_at",
"images",
"responses",
]
read_only_fields = ["id", "created_at", "updated_at", "responses"] read_only_fields = ["id", "created_at", "updated_at", "responses"]
def create(self, validated_data): def create(self, validated_data):
images_data = self.context.get('request').FILES.getlist('images') images_data = self.context.get("request").FILES.getlist("images")
bid = Bid.objects.create(**validated_data) bid = Bid.objects.create(**validated_data)
for image_data in images_data: for image_data in images_data:
# Assuming you have an image upload logic, like storing to S3 and getting a URL # Assuming you have an image upload logic, like storing to S3 and getting a URL
BidImage.objects.create(bid=bid, image=image_data) BidImage.objects.create(bid=bid, image=image_data)
return bid return bid
class AttorneySerializer(serializers.ModelSerializer): class AttorneySerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True) # Nested serializer for the related User object user = UserSerializer(
read_only=True
) # Nested serializer for the related User object
class Meta: class Meta:
model = Attorney model = Attorney
fields = [ fields = [
'user', 'firm_name', 'phone_number', 'address', 'city', "user",
'state', 'zip_code', 'specialties', 'years_experience', 'website', "firm_name",
'profile_picture', 'bio', 'licensed_states', 'created_at', 'updated_at', "phone_number",
"address",
"city",
"state",
"zip_code",
"specialties",
"years_experience",
"website",
"profile_picture",
"bio",
"licensed_states",
"created_at",
"updated_at",
"longitude", "longitude",
"latitude", "latitude",
] ]
read_only_fields = ['created_at', 'updated_at'] read_only_fields = ["created_at", "updated_at"]
def create(self, validated_data): def create(self, validated_data):
# When creating an Attorney, the User object should already exist or be created separately. # When creating an Attorney, the User object should already exist or be created separately.
# This serializer assumes the user is already linked or passed in the context. # This serializer assumes the user is already linked or passed in the context.
# For simplicity, we'll assume the user is passed directly to the view. # For simplicity, we'll assume the user is passed directly to the view.
# In a real scenario, you'd handle user creation/association in the view or a custom manager. # In a real scenario, you'd handle user creation/association in the view or a custom manager.
user_instance = self.context.get('user') user_instance = self.context.get("user")
if not user_instance: if not user_instance:
raise serializers.ValidationError("User instance must be provided to create an Attorney.") raise serializers.ValidationError(
"User instance must be provided to create an Attorney."
)
# Ensure the user_type is correctly set for the new user # Ensure the user_type is correctly set for the new user
if user_instance.user_type != 'attorney': if user_instance.user_type != "attorney":
user_instance.user_type = 'attorney' user_instance.user_type = "attorney"
user_instance.save() user_instance.save()
attorney = Attorney.objects.create(user=user_instance, **validated_data) attorney = Attorney.objects.create(user=user_instance, **validated_data)
@@ -750,47 +952,99 @@ class AttorneySerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
# Handle updates for Attorney fields # Handle updates for Attorney fields
instance.firm_name = validated_data.get('firm_name', instance.firm_name) instance.firm_name = validated_data.get("firm_name", instance.firm_name)
instance.bar_number = validated_data.get('bar_number', instance.bar_number) instance.bar_number = validated_data.get("bar_number", instance.bar_number)
instance.phone_number = validated_data.get('phone_number', instance.phone_number) instance.phone_number = validated_data.get(
instance.address = validated_data.get('address', instance.address) "phone_number", instance.phone_number
instance.city = validated_data.get('city', instance.city) )
instance.state = validated_data.get('state', instance.state) instance.address = validated_data.get("address", instance.address)
instance.zip_code = validated_data.get('zip_code', instance.zip_code) instance.city = validated_data.get("city", instance.city)
instance.specialties = validated_data.get('specialties', instance.specialties) instance.state = validated_data.get("state", instance.state)
instance.years_experience = validated_data.get('years_experience', instance.years_experience) instance.zip_code = validated_data.get("zip_code", instance.zip_code)
instance.website = validated_data.get('website', instance.website) instance.specialties = validated_data.get("specialties", instance.specialties)
instance.profile_picture = validated_data.get('profile_picture', instance.profile_picture) instance.years_experience = validated_data.get(
instance.bio = validated_data.get('bio', instance.bio) "years_experience", instance.years_experience
instance.licensed_states = validated_data.get('licensed_states', instance.licensed_states) )
instance.longitude = validated_data.get('longitude', instance.longitude) instance.website = validated_data.get("website", instance.website)
instance.latitude = validated_data.get('latitude', instance.latitude) instance.profile_picture = validated_data.get(
"profile_picture", instance.profile_picture
)
instance.bio = validated_data.get("bio", instance.bio)
instance.licensed_states = validated_data.get(
"licensed_states", instance.licensed_states
)
instance.longitude = validated_data.get("longitude", instance.longitude)
instance.latitude = validated_data.get("latitude", instance.latitude)
instance.save() instance.save()
return instance return instance
class SupportAgentSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
class Meta:
model = SupportAgent
fields = [
"user",
]
read_only_fields = ["created_at", "updated_at"]
def create(self, validated_data):
user_instance = self.context.get("user")
if not user_instance:
raise serializers.ValidationError(
"User instance must be provided to create a SupportAgent."
)
# Ensure the user_type is correctly set for the new user
if user_instance.user_type != "support_agent":
user_instance.user_type = "support_agent"
user_instance.save()
agent = SupportAgent.objects.create(user=user_instance, **validated_data)
return agent
class RealEstateAgentSerializer(serializers.ModelSerializer): class RealEstateAgentSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True) # Nested serializer for the related User object user = UserSerializer(
read_only=True
) # Nested serializer for the related User object
class Meta: class Meta:
model = RealEstateAgent model = RealEstateAgent
fields = [ fields = [
'user', 'brokerage_name', 'license_number', 'phone_number', 'address', 'city', "user",
'state', 'zip_code', 'specialties', 'years_experience', 'website', "brokerage_name",
'profile_picture', 'bio', 'licensed_states', 'agent_type', 'created_at', 'updated_at', "license_number",
"phone_number",
"address",
"city",
"state",
"zip_code",
"specialties",
"years_experience",
"website",
"profile_picture",
"bio",
"licensed_states",
"agent_type",
"created_at",
"updated_at",
"longitude", "longitude",
"latitude", "latitude",
] ]
read_only_fields = ['created_at', 'updated_at'] read_only_fields = ["created_at", "updated_at"]
def create(self, validated_data): def create(self, validated_data):
user_instance = self.context.get('user') user_instance = self.context.get("user")
if not user_instance: if not user_instance:
raise serializers.ValidationError("User instance must be provided to create a RealEstateAgent.") raise serializers.ValidationError(
"User instance must be provided to create a RealEstateAgent."
)
# Ensure the user_type is correctly set for the new user # Ensure the user_type is correctly set for the new user
if user_instance.user_type != 'real_estate_agent': if user_instance.user_type != "real_estate_agent":
user_instance.user_type = 'real_estate_agent' user_instance.user_type = "real_estate_agent"
user_instance.save() user_instance.save()
agent = RealEstateAgent.objects.create(user=user_instance, **validated_data) agent = RealEstateAgent.objects.create(user=user_instance, **validated_data)
@@ -798,45 +1052,164 @@ class RealEstateAgentSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
# Handle updates for RealEstateAgent fields # Handle updates for RealEstateAgent fields
instance.brokerage_name = validated_data.get('brokerage_name', instance.brokerage_name) instance.brokerage_name = validated_data.get(
instance.license_number = validated_data.get('license_number', instance.license_number) "brokerage_name", instance.brokerage_name
instance.phone_number = validated_data.get('phone_number', instance.phone_number) )
instance.address = validated_data.get('address', instance.address) instance.license_number = validated_data.get(
instance.city = validated_data.get('city', instance.city) "license_number", instance.license_number
instance.state = validated_data.get('state', instance.state) )
instance.zip_code = validated_data.get('zip_code', instance.zip_code) instance.phone_number = validated_data.get(
instance.specialties = validated_data.get('specialties', instance.specialties) "phone_number", instance.phone_number
instance.years_experience = validated_data.get('years_experience', instance.years_experience) )
instance.website = validated_data.get('website', instance.website) instance.address = validated_data.get("address", instance.address)
instance.profile_picture = validated_data.get('profile_picture', instance.profile_picture) instance.city = validated_data.get("city", instance.city)
instance.bio = validated_data.get('bio', instance.bio) instance.state = validated_data.get("state", instance.state)
instance.licensed_states = validated_data.get('licensed_states', instance.licensed_states) instance.zip_code = validated_data.get("zip_code", instance.zip_code)
instance.agent_type = validated_data.get('agent_type', instance.agent_type) instance.specialties = validated_data.get("specialties", instance.specialties)
instance.longitude = validated_data.get('longitude', instance.longitude) instance.years_experience = validated_data.get(
instance.latitude = validated_data.get('latitude', instance.latitude) "years_experience", instance.years_experience
)
instance.website = validated_data.get("website", instance.website)
instance.profile_picture = validated_data.get(
"profile_picture", instance.profile_picture
)
instance.bio = validated_data.get("bio", instance.bio)
instance.licensed_states = validated_data.get(
"licensed_states", instance.licensed_states
)
instance.agent_type = validated_data.get("agent_type", instance.agent_type)
instance.longitude = validated_data.get("longitude", instance.longitude)
instance.latitude = validated_data.get("latitude", instance.latitude)
instance.save() instance.save()
return instance return instance
class PropertySaveSerializer(serializers.ModelSerializer): class PropertySaveSerializer(serializers.ModelSerializer):
""" """
Serializer for the PropertySave model. Serializer for the PropertySave model.
""" """
property = serializers.PrimaryKeyRelatedField(queryset=Property.objects.all()) property = serializers.PrimaryKeyRelatedField(queryset=Property.objects.all())
user = serializers.PrimaryKeyRelatedField(read_only=True) user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta: class Meta:
model = PropertySave model = PropertySave
fields = ['id', 'user', 'property', 'created_at'] fields = ["id", "user", "property", "created_at"]
read_only_fields = ['created_at'] read_only_fields = ["created_at"]
def validate(self, data): def validate(self, data):
""" """
Check for a unique user-property combination before creation. Check for a unique user-property combination before creation.
""" """
user = self.context['request'].user user = self.context["request"].user
property_id = data.get('property').id property_id = data.get("property").id
if PropertySave.objects.filter(user=user, property=property_id).exists(): if PropertySave.objects.filter(user=user, property=property_id).exists():
raise serializers.ValidationError( raise serializers.ValidationError(
"This property is already saved by the user." "This property is already saved by the user."
) )
return data return data
class OfferSerializer(serializers.ModelSerializer):
# parent_offer = 'self'
parent_offer = serializers.SerializerMethodField()
class Meta:
model = OfferDocument
# 'document' field is excluded as it's set by the view after creating the generic Document
exclude = ["document"]
read_only_fields = ["parent_offer"]
def get_parent_offer(self, obj):
if obj.parent_offer:
# Use the same serializer recursively for the parent offer
return OfferSerializer(obj.parent_offer).data
return None
class SellerDisclosureSerializer(serializers.ModelSerializer):
class Meta:
model = SellerDisclosure
fields = [
"general_defects",
"roof_condition",
"roof_age",
"known_roof_leaks",
"plumbing_issues",
"electrical_issues",
"hvac_condition",
"hvac_age",
"known_lead_paint",
"known_asbestos",
"known_radon",
"past_water_damage",
"structural_issues",
"neighborhood_nuisances",
"property_line_disputes",
"appliances_included",
]
# exclude = ['document']
class HomeImprovementReceiptSerializer(serializers.ModelSerializer):
class Meta:
model = HomeImprovementReceipt
exclude = ["document"]
class SupportMessageSerializer(serializers.ModelSerializer):
user_first_name = serializers.CharField(source="user.first_name", read_only=True)
user_last_name = serializers.CharField(source="user.last_name", read_only=True)
class Meta:
model = SupportMessage
fields = [
"id",
"text",
"support_case",
"user",
"created_at",
"updated_at",
"user_first_name",
"user_last_name",
]
read_only_fields = ["created_at", "updated_at", "user"]
class SupportCaseListSerializer(serializers.ModelSerializer):
class Meta:
model = SupportCase
fields = [
"id",
"title",
"description",
"category",
"status",
"user",
"updated_at",
]
read_only_fields = ["created_at", "updated_at", "user"]
class SupportCaseDetailSerializer(serializers.ModelSerializer):
messages = SupportMessageSerializer(many=True, read_only=True)
class Meta:
model = SupportCase
fields = [
"id",
"title",
"description",
"category",
"status",
"user",
"messages",
]
read_only_fields = ["created_at", "updated_at", "user"]
class FAQSerializer(serializers.ModelSerializer):
class Meta:
model = FAQ
fields = ["order", "question", "answer"]

View File

@@ -0,0 +1,64 @@
from core.models import User, Vendor, Bid, OfferDocument, Document
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.template.loader import get_template
from django.template import Context
class EmailService(object):
def __init__(self):
self.from_email: str = "info@ditchtheagent.com"
def send_email(self, subject: str, template_name: str, context: dict, to_email: str | list[str]) -> None:
# NOTE: to_email can be a singular address (str) or a list of emails (list)
# TODO: make a text version of each email
html_content = get_template(f"emails/{template_name}.html").render(context)
text_content = get_template(f"emails/{template_name}.txt").render(context)
to = [to_email] if isinstance(to_email, str) else to_email
msg = EmailMultiAlternatives(subject, text_content, self.from_email, to)
msg.attach_alternative(html_content, "text/html")
msg.send(fail_silently=True)
def send_registration_email(self, user: User, activation_link: str) -> None:
print('Sending a registration email')
context = {
"display_name": user.first_name if user.first_name else user.email,
"activation_link": "This_is@not.com"
}
self.send_email("Account Created", "user_registration_email", context, user.email)
def send_password_reset_email(self, user:User) -> None:
context = {}
self.send_email("Password Reset", "password_reset_email", context, user.email)
def send_password_change_email(self, user:User) -> None:
context = {}
self.send_email("Password Updated", "password_change_email", context, user.email)
def send_new_bid_email(self, bid:Bid, vendors:list[Vendor]) -> None:
context = {"bid_title": bid.bid_type}
emails = Vendor.objects.values_list('user__email', flat=True)
self.send_email("New bid available", "new_bid_email", context, list(emails))
def send_document_shared_email(self, users:list[User]) -> None:
emails = User.objects.values_list('email', flat=True)
context = {}
self.send_email("New document shared with you", "document_shared_email", context, list(emails))
def send_bid_response_email(self, bid:Bid) -> None:
context = {}
self.send_email("New bid response", "bid_response", context, bid.property.property_owner.user.email)
def send_new_offer_email(self, offer: OfferDocument) -> None:
pass
def send_updated_offer_email(self, offer: OfferDocument) -> None:
pass
def send_account_upgrade_email(self, user: User) -> None:
pass
def send_weekly_report_email(self, user:User) -> None:
pass
# TODO: Open house information here

View File

@@ -19,6 +19,10 @@ class PropertyDescriptionGenerator(BaseService):
def _setup_chain(self): def _setup_chain(self):
template = """You are an expert real estate copywriter specializing in creating compelling residential property listings. Write a detailed, engaging description for the following property by incorporating its features, local context, and market appeal. template = """You are an expert real estate copywriter specializing in creating compelling residential property listings. Write a detailed, engaging description for the following property by incorporating its features, local context, and market appeal.
**User Information:**
- User: {owner_name}
- Tier: {owner_tier}
**Property Details:** **Property Details:**
- Address: {address} - Address: {address}
- City: {city} - City: {city}
@@ -31,11 +35,20 @@ class PropertyDescriptionGenerator(BaseService):
- Features: {features_list} - Features: {features_list}
- Coordinates: ({lat}, {lon}) - Coordinates: ({lat}, {lon})
**Tax Information:**
- Tax Amount: {tax_amount}
- Tax Year: {tax_year}
**Walk Score Information:**
- Walk Score: {walk_score}
- Walk Score Description: {walk_description}
- Transit Score: {transit_score}
- Bike Score: {bike_score}
**Instructions:** **Instructions:**
1. First analyze the property's key selling points based on its features, size, and value proposition 1. First analyze the property's key selling points based on its features, size, and value proposition
2. Use the attached [property photos] to note any visible architectural styles, finishes, or unique elements 2. Use the attached [property photos] to note any visible architectural styles, finishes, or unique elements
3. Make API calls (when available) to gather: 3. Make API calls (when available) to gather:
- Walkability score (0-100) from coordinates
- Nearby school ratings (GreatSchools or similar) - Nearby school ratings (GreatSchools or similar)
- Distance/time to major downtown areas - Distance/time to major downtown areas
- Notable nearby amenities (parks, transit, shopping) - Notable nearby amenities (parks, transit, shopping)
@@ -54,7 +67,6 @@ class PropertyDescriptionGenerator(BaseService):
- End with a 'Schedule your showing today!' variation - End with a 'Schedule your showing today!' variation
**API Tools Available (call if needed):** **API Tools Available (call if needed):**
- get_walkability_score(lat, lon)
- get_school_ratings(zip_code) - get_school_ratings(zip_code)
- get_nearby_amenities(lat, lon, radius=1mi) - get_nearby_amenities(lat, lon, radius=1mi)
- get_downtown_distance(lat, lon) - get_downtown_distance(lat, lon)
@@ -76,6 +88,14 @@ class PropertyDescriptionGenerator(BaseService):
"features_list": lambda x: x["features_list"], "features_list": lambda x: x["features_list"],
"lat": lambda x: x["lat"], "lat": lambda x: x["lat"],
"lon": lambda x: x["lon"], "lon": lambda x: x["lon"],
"owner_name": lambda x: x["owner_name"],
"owner_tier": lambda x: x["owner_tier"],
"tax_amount": lambda x: x["tax_amount"],
"tax_year": lambda x: x["tax_year"],
"walk_score": lambda x: x["walk_score"],
"walk_description": lambda x: x["walk_description"],
"transit_score": lambda x: x["transit_score"],
"bike_score": lambda x: x["bike_score"],
} }
| self.prompt | self.prompt
| self.llm | self.llm
@@ -85,6 +105,10 @@ class PropertyDescriptionGenerator(BaseService):
def generate_response( def generate_response(
self, property: Property, **kwargs self, property: Property, **kwargs
) -> Generator[str, None, None]: ) -> Generator[str, None, None]:
owner = property.owner.user
tax_info = getattr(property, "tax_info", None)
walk_score_info = getattr(property, "walk_score", None)
chain_input = { chain_input = {
"address": property.address, "address": property.address,
"city": property.city, "city": property.city,
@@ -97,6 +121,14 @@ class PropertyDescriptionGenerator(BaseService):
"features_list": property.features, "features_list": property.features,
"lat": property.latitude, "lat": property.latitude,
"lon": property.longitude, "lon": property.longitude,
"owner_name": owner.get_full_name(),
"owner_tier": owner.tier,
"tax_amount": getattr(tax_info, "tax_amount", "N/A"),
"tax_year": getattr(tax_info, "year", "N/A"),
"walk_score": getattr(walk_score_info, "walk_score", "N/A"),
"walk_description": getattr(walk_score_info, "walk_description", "N/A"),
"transit_score": getattr(walk_score_info, "transit_score", "N/A"),
"bike_score": getattr(walk_score_info, "bike_score", "N/A"),
} }
return self.conversation_chain.invoke(chain_input) return self.conversation_chain.invoke(chain_input)

View File

@@ -0,0 +1,160 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Email{% endblock %}</title>
<!--
NOTE: For best compatibility, modern email clients prefer inline styles.
This template uses a combination of inline styles and classes.
For production, you may want to use a tool to pre-process the classes into inline styles.
The styles are designed to mimic Material-UI's design language:
- Clean, modern typography (sans-serif)
- Elevated card-like container with rounded corners and a subtle shadow
- Primary color for call-to-action buttons
-->
<style type="text/css">
body,
html {
margin: 0;
padding: 0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
background-color: #2d4a4aff;
color: #050f24;
}
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.card {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 24px;
}
.header {
padding-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 24px;
font-weight: 500;
color: #050f24;
}
.content {
padding-top: 20px;
padding-bottom: 20px;
line-height: 1.6;
font-size: 16px;
}
.content p {
margin: 0 0 16px 0;
}
.button {
display: inline-block;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff !important;
background-color: #2d4a4aff;
border-radius: 4px;
text-decoration: none;
text-align: center;
}
.footer {
padding-top: 20px;
border-top: 1px solid #e0e0e0;
text-align: center;
font-size: 12px;
color: #6f757e;
line-height: 1.5;
}
a {
color: #27d095;
text-decoration: none;
}
</style>
</head>
<body style="margin: 0; padding: 0; background-color: #2d4a4aff">
<div
class="container"
style="width: 100%; max-width: 600px; margin: 0 auto; padding: 20px"
>
<div
class="card"
style="
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 24px;
"
>
<div
class="header"
style="
padding-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
text-align: center;
"
>
<h1
style="
margin: 0;
font-size: 24px;
font-weight: 500;
color: #050f24;
"
>
{% block header_title %}Django App{% endblock %}
</h1>
</div>
<div
class="content"
style="
padding-top: 20px;
padding-bottom: 20px;
line-height: 1.6;
font-size: 16px;
"
>
{% block content %}
<!-- Content will be inserted here by child templates -->
{% endblock %}
</div>
<div
class="footer"
style="
padding-top: 20px;
border-top: 1px solid #e0e0e0;
text-align: center;
font-size: 12px;
color: #6f757e;
line-height: 1.5;
"
>
<p>This email was sent by Ditch the Agent.</p>
<p>Please do not reply to this email.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,29 @@
{% extends 'emails/base_email.html' %} {% block header_title %}Response to Your
Bid{% endblock %} {% block content %}
<p>Hello {{ user.first_name|default:user.username }},</p>
<p>
There has been a new response to your bid titled
<strong>"{{ bid_title }}"</strong>.
</p>
<p>You can view the response by clicking the button below:</p>
<div style="text-align: center; margin: 24px 0">
<a
href="{{ response_link }}"
class="button"
style="
display: inline-block;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff !important;
background-color: #2d4a4aff;
border-radius: 4px;
text-decoration: none;
text-align: center;
"
>
View Response
</a>
</div>
<p>Thank you for using our platform!</p>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends 'emails/base_email.html' %} {% block header_title %}Document Shared
With You{% endblock %} {% block content %}
<p>Hello {{ user.first_name|default:user.username }},</p>
<p>
A document titled <strong>"{{ document_name }}"</strong> has been shared
with you by {{ sharer_name }}.
</p>
<p>You can view the document now by clicking the button below:</p>
<div style="text-align: center; margin: 24px 0">
<a
href="{{ document_link }}"
class="button"
style="
display: inline-block;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff !important;
background-color: #2d4a4aff;
border-radius: 4px;
text-decoration: none;
text-align: center;
"
>
View Document
</a>
</div>
<p>If you have any questions, please contact {{ sharer_name }}.</p>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends 'emails/base_email.html' %} {% block header_title %}New Bid
Available{% endblock %} {% block content %}
<p>Hello {{ user.first_name|default:user.username }},</p>
<p>
A new bid titled <strong>"{{ bid_title }}"</strong> is now available for you
to review and respond to.
</p>
<p>To view the bid details, please click the button below:</p>
<div style="text-align: center; margin: 24px 0">
<a
href="{{ bid_link }}"
class="button"
style="
display: inline-block;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff !important;
background-color: #2d4a4aff;
border-radius: 4px;
text-decoration: none;
text-align: center;
"
>
View Bid
</a>
</div>
<p>We look forward to your response!</p>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'emails/base_email.html' %} {% block header_title %}Password
Changed{% endblock %} {% block content %}
<p>Hello {{ user.first_name|default:user.username }},</p>
<p>
This is an automated confirmation to let you know that the password for your
account was recently changed.
</p>
<p>If you made this change, you can safely ignore this email.</p>
<p>
<strong
>If you did not change your password, please contact support immediately
to secure your account.</strong
>
</p>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends 'emails/base_email.html' %} {% block header_title %}Password Reset
Request{% endblock %} {% block content %}
<p>Hello {{ user.first_name|default:user.username }},</p>
<p>
We received a request to reset the password for your account. If you did not
make this request, you can safely ignore this email.
</p>
<p>To reset your password, please click the link below:</p>
<div style="text-align: center; margin: 24px 0">
<a
href="{{ reset_link }}"
class="button"
style="
display: inline-block;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff !important;
background-color: #2d4a4aff;
border-radius: 4px;
text-decoration: none;
text-align: center;
"
>
Reset Password
</a>
</div>
<p>
If the button above does not work, please copy and paste the following link
into your web browser:
</p>
<p>
<a href="{{ reset_link }}" style="word-break: break-all"
>{{ reset_link }}</a
>
</p>
<p>This link will expire in a few hours for security reasons.</p>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends 'emails/base_email.html' %} {% block header_title %}Welcome to Ditch
The Agent!{% endblock %} {% block content %}
<p>Hello {{ display_name }},</p>
<p>Thank you for registering with us. We're excited to have you on board!</p>
<p>
Please confirm your email address by clicking the button below to activate
your account:
</p>
<div style="text-align: center; margin: 24px 0">
<a
href="{{ activation_link }}"
class="button"
style="
display: inline-block;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff !important;
background-color: #2d4a4aff;
border-radius: 4px;
text-decoration: none;
text-align: center;
"
>
Confirm Account
</a>
</div>
<p>
If the button above does not work, please copy and paste the following link
into your web browser:
</p>
<p>
<a href="{{ activation_link }}" style="word-break: break-all"
>{{ activation_link }}</a
>
</p>
{% endblock %}

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Password Reset</title>
</head>
<body>
<p>Hello {{ user.first_name }},</p>
<p>You're receiving this email because you requested a password reset for your account.</p>
<p>Please click the following link to reset your password:</p>
<p><a href="{{ reset_url }}">{{ reset_url }}</a></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Thanks,<br>
The Real Estate App Team</p>
</body>
</html>

View File

@@ -0,0 +1,8 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from core.views import DocumentViewSet
router = DefaultRouter()
router.register(r"", DocumentViewSet, basename="document")
urlpatterns = router.urls

View File

@@ -1,9 +1,10 @@
from django.urls import path from django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from core.views import PropertyViewSet, PropertyPictureViewSet from core.views import PropertyViewSet, PropertyPictureViewSet,OpenHouseViewSet
router = DefaultRouter() router = DefaultRouter()
router.register(r"pictures", PropertyPictureViewSet, basename="property-pictures") router.register(r"pictures", PropertyPictureViewSet, basename="property-pictures")
router.register(r'open-houses', OpenHouseViewSet)
router.register(r"", PropertyViewSet, basename="property") router.register(r"", PropertyViewSet, basename="property")

View File

@@ -0,0 +1,12 @@
from django.urls import path
from core.public_views import public
urlpatterns = [
path("", public.PropertyList.as_view(), name="property-list"),
path("<int:pk>/", public.PropertyDetail.as_view(), name="property-detail"),
path(
"<int:pk>/increment_view_count/",
public.PropertyIncrementCount.as_view(),
name="property-detail-increment-count",
),
]

View File

@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from core.views import SupportCaseViewSet, SupportMessageViewSet, FAQViewSet
router = DefaultRouter()
router.register(r"cases", SupportCaseViewSet)
router.register(r"messages", SupportMessageViewSet)
router.register(r"faq", FAQViewSet)
urlpatterns = [
path("", include(router.urls)),
]

View File

@@ -0,0 +1,8 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from core.views import SupportAgentViewSet
router = DefaultRouter()
router.register(r"", SupportAgentViewSet, basename="support_agent")
urlpatterns = router.urls

17
dta_service/core/utils.py Normal file
View File

@@ -0,0 +1,17 @@
import math
def haversine_distance(lat1, lon1, lat2, lon2):
R = 3958.8 # Earth radius in miles
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = (
math.sin(dlat / 2) ** 2
+ math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return R * c

View File

@@ -1,6 +1,7 @@
from rest_framework import generics, permissions, status, viewsets from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.db import transaction
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
import requests import requests
@@ -10,19 +11,35 @@ from django.shortcuts import get_object_or_404
from .models import ( from .models import (
PropertyOwner, PropertyOwner,
Vendor, Vendor,
SupportAgent,
Property, Property,
VideoCategory, VideoCategory,
Video, Video,
UserVideoProgress, UserVideoProgress,
Conversation, Conversation,
Message, Message,
Offer, OfferDocument,
PropertyWalkScoreInfo, PropertyWalkScoreInfo,
PropertyTaxInfo, PropertyTaxInfo,
SchoolInfo,Bid, BidResponse, Attorney, RealEstateAgent, UserViewModel, PropertySave SchoolInfo,
Bid,
BidResponse,
Attorney,
RealEstateAgent,
UserViewModel,
PropertySave,
Document,
SellerDisclosure,
HomeImprovementReceipt,
OfferDocument,
OpenHouse,
SupportCase,
SupportMessage,
FAQ,
) )
from .serializers import ( from .serializers import (
CustomTokenObtainPairSerializer, CustomTokenObtainPairSerializer,
SupportAgentSerializer,
UserSerializer, UserSerializer,
UserRegisterSerializer, UserRegisterSerializer,
PropertyOwnerSerializer, PropertyOwnerSerializer,
@@ -37,9 +54,23 @@ from .serializers import (
MessageSerializer, MessageSerializer,
PasswordResetRequestSerializer, PasswordResetRequestSerializer,
PasswordResetConfirmSerializer, PasswordResetConfirmSerializer,
OfferRequestSerializer, # OfferRequestSerializer,
OfferResponseSerializer, # OfferResponseSerializer,
PropertyPictureSerializer, BidSerializer, BidResponseSerializer, AttorneySerializer, RealEstateAgentSerializer, PropertySaveSerializer PropertyPictureSerializer,
BidSerializer,
BidResponseSerializer,
AttorneySerializer,
RealEstateAgentSerializer,
PropertySaveSerializer,
DocumentSerializer,
OfferSerializer,
SellerDisclosureSerializer,
HomeImprovementReceiptSerializer,
OpenHouseSerializer,
SupportMessageSerializer,
SupportCaseDetailSerializer,
SupportCaseListSerializer,
FAQSerializer,
) )
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from .permissions import ( from .permissions import (
@@ -48,12 +79,16 @@ from .permissions import (
IsVendor, IsVendor,
IsParticipant, IsParticipant,
IsParticipantInOffer, IsParticipantInOffer,
IsSupportAgent,
IsPropertyOwnerOrVendorOrAttorney,
) )
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters from rest_framework import filters
from django.db.models import Q from django.db.models import Q, Prefetch
from .services.property_description_generator import PropertyDescriptionGenerator from .services.property_description_generator import PropertyDescriptionGenerator
from .filters import PropertyFilterSet from .filters import PropertyFilterSet, VendorFilterSet
from .services.email_service import EmailService
User = get_user_model() User = get_user_model()
@@ -67,6 +102,25 @@ class UserRegisterView(generics.CreateAPIView):
serializer_class = UserRegisterSerializer serializer_class = UserRegisterSerializer
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
def perform_create(self, serializer):
# This will save the user instance and return the object
user = serializer.save()
# Here you would typically generate an activation link.
# This is a placeholder as the exact link generation depends on your
# front-end URL and activation logic (e.g., using a token).
# For a full implementation, you would need to generate a unique token
# and include it in the URL.
# activation_link = "http://your-frontend-url.com/activate/"
# Call the email-sending function with the newly created user object
# and the activation link.
EmailService.send_registration_email(user, activation_link)
# You can optionally modify the response data here if needed.
# For example, to not return all user data.
return Response(serializer.data, status=status.HTTP_201_CREATED)
class UserRetrieveView(generics.RetrieveAPIView, generics.UpdateAPIView): class UserRetrieveView(generics.RetrieveAPIView, generics.UpdateAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -114,6 +168,47 @@ class LogoutView(APIView):
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
class OpenHouseViewSet(viewsets.ModelViewSet):
"""
A ViewSet for viewing and editing OpenHouse instances.
"""
queryset = OpenHouse.objects.all()
serializer_class = OpenHouseSerializer
permission_classes = [IsAuthenticated, IsPropertyOwner]
def get_queryset(self):
"""
This view should return a list of all the open houses
for the currently authenticated property owner.
"""
user = self.request.user
if user.is_authenticated and hasattr(user, "propertyowner"):
return OpenHouse.objects.filter(
property__owner=user.propertyowner
).order_by("start_time")
return OpenHouse.objects.none()
def perform_create(self, serializer):
"""
Ensures that the property being assigned to the open house belongs
to the authenticated property owner.
"""
property_id = self.request.data.get("property")
try:
property_instance = Property.objects.get(
id=property_id, owner__user=self.request.user
)
serializer.save(property=property_instance)
except Property.DoesNotExist:
return Response(
{
"detail": "You do not have permission to schedule an open house for this property."
},
status=status.HTTP_403_FORBIDDEN,
)
class PasswordResetRequestView(APIView): class PasswordResetRequestView(APIView):
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
@@ -151,7 +246,7 @@ class PropertyOwnerViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
if user.user_type == "property_owner": if user.user_type == "property_owner":
if(PropertyOwner.objects.filter(user=user).count() == 0): if PropertyOwner.objects.filter(user=user).count() == 0:
return PropertyOwner.objects.create( return PropertyOwner.objects.create(
user=user, user=user,
) )
@@ -164,6 +259,7 @@ class VendorViewSet(viewsets.ModelViewSet):
serializer_class = VendorSerializer serializer_class = VendorSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter] filter_backends = [DjangoFilterBackend, filters.SearchFilter]
filterset_class = VendorFilterSet
search_fields = [ search_fields = [
"business_name", "business_name",
"user__first_name", "user__first_name",
@@ -174,7 +270,7 @@ class VendorViewSet(viewsets.ModelViewSet):
lookup_field = "user__id" # or 'user__id' if you want to be explicit lookup_field = "user__id" # or 'user__id' if you want to be explicit
def get_permissions(self): def get_permissions(self):
if self.action in ['increment_view_count', 'increment_save_count']: if self.action in ["increment_view_count", "increment_save_count"]:
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
else: else:
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
@@ -201,24 +297,21 @@ class VendorViewSet(viewsets.ModelViewSet):
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
# The update method will now handle the vendor profile correctly # The update method will now handle the vendor profile correctly
# and ignore any user data in the payload. # and ignore any user data in the payload.
partial = kwargs.pop('partial', False) partial = kwargs.pop("partial", False)
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
self.perform_update(serializer) self.perform_update(serializer)
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=['post']) @action(detail=True, methods=["post"])
def increment_view_count(self, request, user__id=None): def increment_view_count(self, request, user__id=None):
vendor_obj = Vendor.objects.get(user__id=user__id) vendor_obj = Vendor.objects.get(user__id=user__id)
vendor_obj.views += 1 vendor_obj.views += 1
vendor_obj.save() vendor_obj.save()
UserViewModel.objects.create( UserViewModel.objects.create(user_id=user__id)
user_id=user__id
)
return Response({'views': vendor_obj.views}, status=status.HTTP_200_OK)
return Response({"views": vendor_obj.views}, status=status.HTTP_200_OK)
# Attorney ViewSet # Attorney ViewSet
@@ -243,6 +336,7 @@ class AttorneyViewSet(viewsets.ModelViewSet):
else: else:
return Attorney.objects.all() return Attorney.objects.all()
# Real Estate Agent ViewSet # Real Estate Agent ViewSet
class RealEstateAgentViewSet(viewsets.ModelViewSet): class RealEstateAgentViewSet(viewsets.ModelViewSet):
serializer_class = RealEstateAgentSerializer serializer_class = RealEstateAgentSerializer
@@ -262,13 +356,28 @@ class RealEstateAgentViewSet(viewsets.ModelViewSet):
return RealEstateAgent.objects.all() return RealEstateAgent.objects.all()
class SupportAgentViewSet(viewsets.ModelViewSet):
serializer_class = SupportAgentSerializer
permission_classes = [IsSupportAgent]
def perform_create(self, serializer):
# Link to the currently authenticated user
serializer.save(user=self.request.user)
def get_queryset(self):
user = self.request.user
if not SupportAgent.objects.filter(user=user).exists():
return SupportAgent.objects.create(user=user)
return SupportAgent.objects.filter(user=user)
class PropertyViewSet(viewsets.ModelViewSet): class PropertyViewSet(viewsets.ModelViewSet):
filter_backends = [DjangoFilterBackend, filters.SearchFilter] filter_backends = [DjangoFilterBackend, filters.SearchFilter]
filterset_class = PropertyFilterSet filterset_class = PropertyFilterSet
search_fields = ["address", "city", "state", "zip_code"] search_fields = ["address", "city", "state", "zip_code"]
def get_permissions(self): def get_permissions(self):
if self.action in ['increment_view_count', 'increment_save_count']: if self.action in ["increment_view_count", "increment_save_count"]:
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
else: else:
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
@@ -299,35 +408,43 @@ class PropertyViewSet(viewsets.ModelViewSet):
return Property.objects.all() return Property.objects.all()
def perform_create(self, serializer): def perform_create(self, serializer):
if self.request.user.user_type == "property_owner": if self.request.user.user_type == "property_owner":
owner = PropertyOwner.objects.get(user=self.request.user) owner = PropertyOwner.objects.get(user=self.request.user)
## attempt to get the walkscore ## attempt to get the walkscore
res = requests.get(f'https://api.walkscore.com/score?format=json&address={self.request.data['address']}&lat={self.request.data['latitude']}&lon={self.request.data['longitude']}&transit=1&bike=1&wsapikey={'4430c9adf62a4d93cd1efbdcd018b4d4'}') res = requests.get(
f"https://api.walkscore.com/score?format=json&address={self.request.data['address']}&lat={self.request.data['latitude']}&lon={self.request.data['longitude']}&transit=1&bike=1&wsapikey={'4430c9adf62a4d93cd1efbdcd018b4d4'}"
)
if res.ok: if res.ok:
data = res.json() data = res.json()
has_transit = data.get('transit') has_transit = data.get("transit")
has_bike = data.get('bike') has_bike = data.get("bike")
walk_score = PropertyWalkScoreInfo.objects.create( walk_score = PropertyWalkScoreInfo.objects.create(
walk_score = data.get('walkscore'), walk_score=data.get("walkscore"),
walk_description = data.get('description'), walk_description=data.get("description"),
ws_link = data.get('ws_link'), ws_link=data.get("ws_link"),
logo_url = data.get('logo_url'), logo_url=data.get("logo_url"),
transit_score = data.get('transit').get('score') if has_transit else None, transit_score=data.get("transit").get("score")
transit_description = data.get('transit').get('description') if has_transit else None, if has_transit
transit_summary = data.get('transit').get('summary') if has_transit else None, else None,
bike_score = data.get('bike').get('score') if has_bike else None, transit_description=data.get("transit").get("description")
bike_description = data.get('bike').get('description') if has_bike else None, if has_transit
else None,
transit_summary=data.get("transit").get("summary")
if has_transit
else None,
bike_score=data.get("bike").get("score") if has_bike else None,
bike_description=data.get("bike").get("description")
if has_bike
else None,
) )
serializer.save(owner=owner, walk_score=walk_score) serializer.save(owner=owner, walk_score=walk_score)
else: else:
serializer.save(owner=owner) serializer.save(owner=owner)
else: else:
serializer.save() serializer.save()
@action(detail=True, methods=['post'])
@action(detail=True, methods=["post"])
def increment_view_count(self, request, pk=None): def increment_view_count(self, request, pk=None):
property_obj = self.get_object() property_obj = self.get_object()
property_obj.views += 1 property_obj.views += 1
@@ -336,14 +453,14 @@ class PropertyViewSet(viewsets.ModelViewSet):
# UserViewModel.objects.create( # UserViewModel.objects.create(
# user__id=pk # user__id=pk
# ) # )
return Response({'views': property_obj.views}, status=status.HTTP_200_OK) return Response({"views": property_obj.views}, status=status.HTTP_200_OK)
@action(detail=True, methods=['post']) @action(detail=True, methods=["post"])
def increment_save_count(self, request, pk=None): def increment_save_count(self, request, pk=None):
property_obj = self.get_object() property_obj = self.get_object()
property_obj.saves += 1 property_obj.saves += 1
property_obj.save() property_obj.save()
return Response({'saves': property_obj.saves}, status=status.HTTP_200_OK) return Response({"saves": property_obj.saves}, status=status.HTTP_200_OK)
class PropertyPictureViewSet(viewsets.ModelViewSet): class PropertyPictureViewSet(viewsets.ModelViewSet):
@@ -395,7 +512,6 @@ class VideoViewSet(viewsets.ModelViewSet):
class UserVideoProgressViewSet(viewsets.ModelViewSet): class UserVideoProgressViewSet(viewsets.ModelViewSet):
serializer_class = UserVideoProgressSerializer serializer_class = UserVideoProgressSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -414,7 +530,6 @@ class UserVideoProgressViewSet(viewsets.ModelViewSet):
class ConversationViewSet(viewsets.ModelViewSet): class ConversationViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsParticipant] permission_classes = [IsAuthenticated, IsParticipant]
search_fields = ["vendor", "property_owner"] search_fields = ["vendor", "property_owner"]
@@ -453,8 +568,6 @@ class ConversationViewSet(viewsets.ModelViewSet):
serializer.save(vendor=vendor) serializer.save(vendor=vendor)
class MessageViewSet(viewsets.ModelViewSet): class MessageViewSet(viewsets.ModelViewSet):
serializer_class = MessageSerializer serializer_class = MessageSerializer
permission_classes = [IsAuthenticated, IsParticipant] permission_classes = [IsAuthenticated, IsParticipant]
@@ -477,61 +590,46 @@ class MessageViewSet(viewsets.ModelViewSet):
return context return context
class OfferViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsParticipantInOffer]
def get_serializer_class(self):
"""
Returns the serializer class to use depending on the action.
- For 'list' and 'retrieve' (read operations), use PropertyResponseSerializer.
- For 'create', 'update', 'partial_update' (write operations), use PropertyRequestSerializer.
"""
if self.action in ["list", "retrieve"]:
return OfferResponseSerializer
return OfferRequestSerializer
def get_queryset(self):
user_lookup = Q(user=self.request.user)
property_lookup = Q(property__owner__user=self.request.user)
null_previous_offer = Q(previous_offer=None)
return Offer.objects.filter(
(property_lookup & null_previous_offer)
| (user_lookup & null_previous_offer)
)
class BidViewSet(viewsets.ModelViewSet): class BidViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
if user.user_type == 'property_owner': if user.user_type == "property_owner":
return Bid.objects.filter(property__owner__user=user).order_by('-created_at') return Bid.objects.filter(property__owner__user=user).order_by(
elif user.user_type == 'vendor': "-created_at"
)
elif user.user_type == "vendor":
# Vendors should see all bids, but only their own responses # Vendors should see all bids, but only their own responses
return Bid.objects.all().order_by('-created_at') return Bid.objects.all().order_by("-created_at")
return Bid.objects.none() return Bid.objects.none()
def get_serializer_class(self): def get_serializer_class(self):
return BidSerializer return BidSerializer
@action(detail=True, methods=['post']) @action(detail=True, methods=["post"])
def select_response(self, request, pk=None): def select_response(self, request, pk=None):
bid = self.get_object() bid = self.get_object()
response_id = request.data.get('response_id') response_id = request.data.get("response_id")
try: try:
response = BidResponse.objects.get(id=response_id, bid=bid) response = BidResponse.objects.get(id=response_id, bid=bid)
# Ensure the current user is the property owner of the bid # Ensure the current user is the property owner of the bid
if request.user == bid.property.owner.user: if request.user == bid.property.owner.user:
# Unselect any previously selected response for this bid # Unselect any previously selected response for this bid
BidResponse.objects.filter(bid=bid, status='selected').update(status='submitted') BidResponse.objects.filter(bid=bid, status="selected").update(
status="submitted"
)
# Select the new response # Select the new response
response.status = 'selected' response.status = "selected"
response.save() response.save()
return Response({'status': 'response selected'}) return Response({"status": "response selected"})
return Response({'error': 'You do not have permission to perform this action.'}, status=403) return Response(
{"error": "You do not have permission to perform this action."},
status=403,
)
except BidResponse.DoesNotExist: except BidResponse.DoesNotExist:
return Response({'error': 'Response not found.'}, status=404) return Response({"error": "Response not found."}, status=404)
class BidResponseViewSet(viewsets.ModelViewSet): class BidResponseViewSet(viewsets.ModelViewSet):
serializer_class = BidResponseSerializer serializer_class = BidResponseSerializer
@@ -539,31 +637,34 @@ class BidResponseViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
if user.user_type == 'property_owner': if user.user_type == "property_owner":
return BidResponse.objects.filter(bid__property__owner__user=user).order_by('-created_at') return BidResponse.objects.filter(bid__property__owner__user=user).order_by(
elif user.user_type == 'vendor': "-created_at"
return BidResponse.objects.filter(vendor__user=user).order_by('-created_at') )
elif user.user_type == "vendor":
return BidResponse.objects.filter(vendor__user=user).order_by("-created_at")
return BidResponse.objects.none() return BidResponse.objects.none()
def perform_create(self, serializer): def perform_create(self, serializer):
# A vendor can only create one response per bid # A vendor can only create one response per bid
bid = serializer.validated_data['bid'] bid = serializer.validated_data["bid"]
vendor = self.request.user.vendor vendor = self.request.user.vendor
if BidResponse.objects.filter(bid=bid, vendor=vendor).exists(): if BidResponse.objects.filter(bid=bid, vendor=vendor).exists():
raise serializers.ValidationError("You have already responded to this bid.") raise serializers.ValidationError("You have already responded to this bid.")
serializer.save(vendor=vendor, status='submitted') serializer.save(vendor=vendor, status="submitted")
class PropertySaveViewSet( class PropertySaveViewSet(
viewsets.mixins.CreateModelMixin, viewsets.mixins.CreateModelMixin,
viewsets.mixins.ListModelMixin, viewsets.mixins.ListModelMixin,
viewsets.mixins.DestroyModelMixin, viewsets.mixins.DestroyModelMixin,
viewsets.GenericViewSet viewsets.GenericViewSet,
): ):
""" """
A viewset that provides 'create', 'list', and 'destroy' actions A viewset that provides 'create', 'list', and 'destroy' actions
for saved properties. for saved properties.
""" """
queryset = PropertySave.objects.all() queryset = PropertySave.objects.all()
serializer_class = PropertySaveSerializer serializer_class = PropertySaveSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -574,13 +675,17 @@ class PropertySaveViewSet(
for the currently authenticated user. for the currently authenticated user.
""" """
user = self.request.user user = self.request.user
return PropertySave.objects.filter(user=user).order_by('-created_at') return PropertySave.objects.filter(user=user).order_by("-created_at")
def perform_create(self, serializer): def perform_create(self, serializer):
""" """
Saves the new PropertySave instance, associating it with Saves the new PropertySave instance, associating it with
the current authenticated user. the current authenticated user.
""" """
# update the save count for the property
property = serializer.validated_data.get("property")
property.saves += 1
property.save()
serializer.save(user=self.request.user) serializer.save(user=self.request.user)
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
@@ -589,13 +694,370 @@ class PropertySaveViewSet(
""" """
try: try:
instance = self.get_object() instance = self.get_object()
property = instance.property
property.saves -= 1
property.save()
self.perform_destroy(instance) self.perform_destroy(instance)
return Response( return Response(
{"detail": "Property successfully unsaved."}, {"detail": "Property successfully unsaved."},
status=status.HTTP_204_NO_CONTENT status=status.HTTP_204_NO_CONTENT,
) )
except PropertySave.DoesNotExist: except PropertySave.DoesNotExist:
return Response( return Response(
{"detail": "PropertySave instance not found."}, {"detail": "PropertySave instance not found."},
status=status.HTTP_404_NOT_FOUND status=status.HTTP_404_NOT_FOUND,
) )
class DocumentViewSet(viewsets.ModelViewSet):
serializer_class = DocumentSerializer
permission_classes = [IsAuthenticated, IsPropertyOwnerOrVendorOrAttorney]
search_fields = [
"document_type",
]
def get_queryset(self):
# Only allow users to see documents for properties they are associated with.
user = self.request.user
if user.user_type == "property_owner":
# Filter for documents the user can see
# documents = Document.objects.filter(
# Q(property__owner__user=user) | Q(uploaded_by=user) | Q(shared_with=user)
# )#.exclude(offer_data__parent_offer__isnull=False)
# # Prefetch related OfferDocument data
# offers = OfferDocument.objects.filter(parent_offer__isnull=True)
# offers_prefetch = Prefetch('offer_data', queryset=offers)
# documents = documents.prefetch_related(offers_prefetch)
return Document.objects.filter(
(
Q(property__owner__user=user)
| Q(uploaded_by=user)
| Q(shared_with=user)
)
& (
Q(offer_data__isnull=True)
| Q(offer_data__counter_offers__isnull=True)
)
).distinct()
# Add more logic here for vendors and attorneys to see relevant documents
# For example, for an attorney associated with a property:
# return Document.objects.filter(property__attorney__user=user)
# For now, let's keep it simple.
return Document.objects.none()
def perform_create(self, serializer):
# Automatically link the uploader
serializer.save(uploaded_by=self.request.user)
class RetrieveDocumentView(generics.RetrieveAPIView, generics.UpdateAPIView):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
permission_classes = [IsAuthenticated]
lookup_field = "id"
def get_object(self):
# Override get_object to use a query parameter.
document_id = self.request.query_params.get("docId")
if not document_id:
raise NotFound(detail="docId is required.")
try:
# We use select_related to eagerly load the related documents
# to prevent extra database queries in the serializer.
return Document.objects.select_related(
"offer_data", "seller_disclosure_data", "home_improvement_receipt_data"
).get(id=document_id)
except Document.DoesNotExist:
raise NotFound(detail="Document not found.")
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
# Return the data directly from the serializer.
return Response(serializer.data)
# def get(self, request, *args, **kwargs):
# print(request, args, kwargs)
# documentId = request.query_params.get('docId', None)
# if not documentId:
# return Response(
# {"detail": "docId is required."},
# status=status.HTTP_400_BAD_REQUEST
# )
# try:
# document = Document.objects.get(id=documentId)
# document_serializer = DocumentSerializer(document)
# documentType = document.document_type
# if documentType == 'seller_disclosure':
# serializer = SellerDisclosureSerializer(SellerDisclosure.objects.get(document_id=documentId))
# elif documentType == 'offer_letter':
# serializer = OfferSerializer(OfferDocument.objects.get(document_id=documentId))
# elif documentType == 'home_improvement_receipt':
# serializer = HomeImprovementReceiptSerializer(HomeImprovementReceipt.objects.get(document_id=documentId))
# else:
# return Response({'error': 'couldnt find the document'}, status=status.HTTP_400_BAD_REQUEST)
# data = {
# "document": document_serializer.data
# }
# data['document']['sub_document'] = serializer.data
# return Response(data=data, status=status.HTTP_200_OK)
# except Exception as e:
# return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, *args, **kwargs):
print(request, args, kwargs)
document_id = request.data.get("document_id")
action = request.data.get("action")
if not document_id or not action:
return Response(
{"error": "Need to supply both document_id and action"},
status=status.HTTP_400_BAD_REQUEST,
)
try:
with transaction.atomic():
document = Document.objects.get(id=document_id)
if document.document_type == "offer_letter":
offer_document = OfferDocument.objects.get(document__id=document_id)
if action == "accept":
# find all of the other documents that pertain to this property and reject them
other_docs = Document.objects.filter(
property=document.property, document_type="offer_letter"
).exclude(id=document_id)
for other_doc in other_docs:
offer_doc = OfferDocument.objects.get(
document__id=other_doc.id
)
offer_doc.status = "rejected"
offer_doc.save()
offer_document.status = "accepted"
offer_document.save()
elif action == "reject":
offer_document.status = "rejected"
offer_document.save()
elif action == "counter":
offer_document.status = "countered"
offer_document.save()
# Create a new generic Document for the counter-offer
new_doc = Document.objects.create(
property=document.property,
document_type="offer_letter",
description=f"Counter-offer to document ID {document.id}",
uploaded_by=request.user,
shared_with=[document.uploaded_by],
)
# Create the new OfferDocument, linking it to the new generic document
# and the parent offer.
new_offer_doc = OfferDocument.objects.create(
document=new_doc,
parent_offer=offer_document,
offer_price=Decimal(str(new_price)),
closing_date=new_closing_date,
closing_days=new_closing_days,
contingencies=new_contingencies or "",
status="pending", # Set the status of the new offer
)
# Return the newly created offer data
serializer = OfferSerializer(new_offer_doc)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(
{"error": f"Dont understand the action: {action}"},
status=status.HTTP_400_BAD_REQUEST,
)
else:
return Response(
{"error": "functionality is not implemented"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(data={}, status=status.HTTP_200_OK)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
class CreateDocumentView(APIView):
# Apply authentication if needed. For example, IsAuthenticated will ensure only logged-in users can upload.
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
# Validate property_id and document_type first
property_id = request.data.get("property")
document_type = request.data.get("document_type")
# Ensure property_id and document_type are provided
if not property_id:
return Response(
{"detail": "Property ID is required."},
status=status.HTTP_400_BAD_REQUEST,
)
if not document_type:
return Response(
{"detail": "Document type is required."},
status=status.HTTP_400_BAD_REQUEST,
)
property = Property.objects.get(id=property_id)
# Check if the document_type is one of our specific types that require extra data
specific_document_types = {
"offer": OfferSerializer,
"seller_disclosure": SellerDisclosureSerializer,
"home_improvement_receipt": HomeImprovementReceiptSerializer,
}
# Validate the generic Document data
# 'uploaded_by' can be automatically set to the current user
request.data["uploaded_by"] = (
request.user.id if request.user.is_authenticated else None
)
# Initialize DocumentSerializer with all data, including file
# Make a mutable copy of request.data if it's an immutable QueryDict
mutable_data = request.data.copy()
print(mutable_data)
if mutable_data["document_type"] == "offer":
mutable_data["document_type"] = "offer_letter"
# smartly create the description
if document_type == "offer":
mutable_data["description"] = (
f"${mutable_data['offer_price']} Offer for {property.address}"
)
elif document_type == "seller_disclosure":
mutable_data["description"] = property.address
document_serializer = DocumentSerializer(data=mutable_data)
with transaction.atomic():
if document_serializer.is_valid():
try:
# Save the generic Document instance
document = document_serializer.save()
# If it's a specific type, validate and save its data
if document_type in specific_document_types:
specific_serializer_class = specific_document_types[
document_type
]
specific_data = (
request.data.copy()
) # Use a copy for specific serializer
# Remove generic document fields that are not part of specific serializers
# This ensures the specific serializer only sees its relevant fields
for field in document_serializer.fields:
if field in specific_data:
del specific_data[field]
specific_serializer = specific_serializer_class(
data=specific_data
)
if specific_serializer.is_valid():
specific_instance = specific_serializer.save(
document=document
) # Link to the document
# Prepare response data, including both generic and specific data
response_data = document_serializer.data
response_data[f"{document_type}_data"] = (
specific_serializer.data
)
return Response(
response_data, status=status.HTTP_201_CREATED
)
else:
# If specific data is invalid, roll back the generic document creation
try:
# attempt to remove the document if it is already there
document.delete()
except:
pass
return Response(
{
"detail": f"Invalid data for {document_type}.",
"errors": specific_serializer.errors,
},
status=status.HTTP_400_BAD_REQUEST,
)
else:
# For generic document types (e.g., 'other', 'attorney_contract')
return Response(
document_serializer.data, status=status.HTTP_201_CREATED
)
except Exception as e:
# Catch any unexpected errors during transaction
return Response(
{"detail": f"An unexpected error occurred: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
else:
# If generic document data is invalid
return Response(
document_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
class SupportCaseViewSet(viewsets.ModelViewSet):
queryset = SupportCase.objects.all()
permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self):
if self.action == "retrieve":
return SupportCaseDetailSerializer
return SupportCaseListSerializer
def get_queryset(self):
user = self.request.user
if user.user_type == "support_agent":
queryset = SupportCase.objects.all()
else:
queryset = SupportCase.objects.filter(user=user)
status = self.request.query_params.get("status")
if status:
queryset = queryset.filter(status=status)
category = self.request.query_params.get("category")
if category:
queryset = queryset.filter(category=category)
return queryset
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class SupportMessageViewSet(viewsets.ModelViewSet):
queryset = SupportMessage.objects.all()
serializer_class = SupportMessageSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
if user.user_type == "support_agent":
return SupportMessage.objects.all()
return SupportMessage.objects.filter(support_case__user=user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class FAQViewSet(viewsets.ModelViewSet):
queryset = FAQ.objects.all()
serializer_class = FAQSerializer
permission_classes = [permissions.IsAuthenticated]

View File

@@ -200,14 +200,11 @@ SIMPLE_JWT = {
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
} }
# Email settings EMAIL_HOST = "mail.smtp2go.com"
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST_USER = "info@ditchtheagent.com"
# EMAIL_HOST = config('SMTP2GO_HOST') EMAIL_HOST_PASSWORD = "AkvsvMblPTCLJQGW"
# EMAIL_PORT = config('SMTP2GO_PORT', cast=int) EMAIL_PORT = 2525
# EMAIL_USE_TLS = True EMAIL_USE_TLS = True
# EMAIL_HOST_USER = config('SMTP2GO_USERNAME')
# EMAIL_HOST_PASSWORD = config('SMTP2GO_PASSWORD')
# DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL')
# CHANNEL_LAYERS = { # CHANNEL_LAYERS = {

View File

@@ -14,54 +14,84 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from rest_framework_simplejwt.views import ( from rest_framework_simplejwt.views import (
TokenRefreshView, TokenRefreshView,
) )
from core.views import ( from core.views import (
CustomTokenObtainPairView, LogoutView, CustomTokenObtainPairView,
PasswordResetRequestView, PasswordResetConfirmView, LogoutView,
UserRegisterView, UserRetrieveView, UserSignTosView, PropertyDescriptionView PasswordResetRequestView,
PasswordResetConfirmView,
UserRegisterView,
UserRetrieveView,
UserSignTosView,
PropertyDescriptionView,
CreateDocumentView,
RetrieveDocumentView,
) )
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
# Authentication # Authentication
path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), path("api/token/", CustomTokenObtainPairView.as_view(), name="token_obtain_pair"),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path('api/logout/', LogoutView.as_view(), name='logout'), path("api/logout/", LogoutView.as_view(), name="logout"),
path('api/register/', UserRegisterView.as_view(), name='register'), path("api/register/", UserRegisterView.as_view(), name="register"),
path('api/password-reset/', PasswordResetRequestView.as_view(), name='password_reset'), path(
path('api/password-reset/confirm/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), "api/password-reset/", PasswordResetRequestView.as_view(), name="password_reset"
),
path(
"api/password-reset/confirm/",
PasswordResetConfirmView.as_view(),
name="password_reset_confirm",
),
# API endpoints # API endpoints
path('api/user/', UserRetrieveView.as_view(), name='get_user'), path("api/attorney/", include("core.urls.attorney")),
path('api/user/acknowledge_tos/', UserSignTosView.as_view(), name='sign_tos'), path("api/document/", include("core.urls.document")),
path('api/property-description-generator/<int:property_id>/', PropertyDescriptionView.as_view(), name='property-description-generator'), path("api/conversations/", include("core.urls.conversation")),
path('api/property-owners/', include('core.urls.property_owner')), # path('api/offers/', include('core.urls.offer')),
path('api/vendors/', include('core.urls.vendor')), path("api/properties/", include("core.urls.property")),
path('api/properties/', include('core.urls.property')), path(
path('api/videos/', include('core.urls.video')), "api/property-description-generator/<int:property_id>/",
path('api/conversations/', include('core.urls.conversation')), PropertyDescriptionView.as_view(),
path('api/offers/', include('core.urls.offer')), name="property-description-generator",
path('api/attorney/', include('core.urls.attorney')), ),
path('api/real_estate_agent/', include('core.urls.real_estate_agent')), path("api/property-owners/", include("core.urls.property_owner")),
path('api/saved-properties/', include('core.urls.property_save')), path("api/real_estate_agent/", include("core.urls.real_estate_agent")),
path('api/', include('core.urls.bid')), path("api/support_agent/", include("core.urls.support_agent")),
path("api/saved-properties/", include("core.urls.property_save")),
path("api/user/", UserRetrieveView.as_view(), name="get_user"),
path("api/user/acknowledge_tos/", UserSignTosView.as_view(), name="sign_tos"),
path("api/vendors/", include("core.urls.vendor")),
path("api/support/", include("core.urls.support")),
path("api/videos/", include("core.urls.video")),
path("api/", include("core.urls.bid")),
path("api/public/", include("core.urls.public")),
path("api/documents/upload/", CreateDocumentView.as_view(), name="document-upload"),
path(
"api/documents/retrieve/",
RetrieveDocumentView.as_view(),
name="document-retrieve",
),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
def print_patterns(patterns, base_path=""): def print_patterns(patterns, base_path=""):
for pattern in patterns: for pattern in patterns:
if hasattr(pattern, 'url_patterns'): # It's a URLResolver if hasattr(pattern, "url_patterns"): # It's a URLResolver
print_patterns(pattern.url_patterns, base_path + str(pattern.pattern)) print_patterns(pattern.url_patterns, base_path + str(pattern.pattern))
else: # It's a URLPattern else: # It's a URLPattern
full_path = base_path + str(pattern.pattern) full_path = base_path + str(pattern.pattern)
view_name = pattern.callback.__module__ + "." + pattern.callback.__name__ view_name = pattern.callback.__module__ + "." + pattern.callback.__name__
print(f"URL: /{full_path.replace('^', '').replace('$', '')} -> View: {view_name}") print(
f"URL: /{full_path.replace('^', '').replace('$', '')} -> View: {view_name}"
)
print_patterns(urlpatterns) print_patterns(urlpatterns)