checking in for production
This commit is contained in:
@@ -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",
|
||||||
)
|
)
|
||||||
readonly_fields = ('created_at', 'updated_at')
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"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")
|
||||||
|
|
||||||
|
|
||||||
# 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",
|
||||||
|
"license_number",
|
||||||
|
"phone_number",
|
||||||
|
"city",
|
||||||
|
"state",
|
||||||
|
"agent_type",
|
||||||
|
"years_experience",
|
||||||
|
)
|
||||||
|
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')
|
# filter_horizontal = ('specialties', 'licensed_states')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(
|
||||||
'fields': ('user', 'brokerage_name', 'license_number', 'phone_number', 'address', 'city', 'state', 'zip_code', 'bio', 'profile_picture', 'website', 'agent_type')
|
None,
|
||||||
}),
|
{
|
||||||
('Professional Details', {
|
"fields": (
|
||||||
'fields': ('specialties', 'years_experience', 'licensed_states')
|
"user",
|
||||||
}),
|
"brokerage_name",
|
||||||
('Location', {
|
"license_number",
|
||||||
'fields': ('latitude', 'longitude'),
|
"phone_number",
|
||||||
}),
|
"address",
|
||||||
|
"city",
|
||||||
('Timestamps', {
|
"state",
|
||||||
'fields': ('created_at', 'updated_at'),
|
"zip_code",
|
||||||
'classes': ('collapse',)
|
"bio",
|
||||||
}),
|
"profile_picture",
|
||||||
|
"website",
|
||||||
|
"agent_type",
|
||||||
)
|
)
|
||||||
readonly_fields = ('created_at', 'updated_at')
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"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)
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
74
dta_service/core/migrations/0024_document.py
Normal file
74
dta_service/core/migrations/0024_document.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
|
),
|
||||||
|
]
|
||||||
20
dta_service/core/migrations/0026_alter_document_file.py
Normal file
20
dta_service/core/migrations/0026_alter_document_file.py
Normal 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/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
dta_service/core/migrations/0027_offerdocument_status.py
Normal file
28
dta_service/core/migrations/0027_offerdocument_status.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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/"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,14 +152,16 @@ 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)
|
||||||
@@ -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,7 +197,9 @@ 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)
|
||||||
@@ -177,7 +211,9 @@ class RealEstateAgent(models.Model):
|
|||||||
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
0
dta_service/core/public_views/__init__.py
Normal file
0
dta_service/core/public_views/__init__.py
Normal file
107
dta_service/core/public_views/public.py
Normal file
107
dta_service/core/public_views/public.py
Normal 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)
|
||||||
@@ -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"]
|
||||||
|
|||||||
64
dta_service/core/services/email_service.py
Normal file
64
dta_service/core/services/email_service.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
160
dta_service/core/templates/emails/base_template.html
Normal file
160
dta_service/core/templates/emails/base_template.html
Normal 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>
|
||||||
29
dta_service/core/templates/emails/bid_response.html
Normal file
29
dta_service/core/templates/emails/bid_response.html
Normal 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 %}
|
||||||
29
dta_service/core/templates/emails/document_shared_email.html
Normal file
29
dta_service/core/templates/emails/document_shared_email.html
Normal 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 %}
|
||||||
29
dta_service/core/templates/emails/new_bid_email.html
Normal file
29
dta_service/core/templates/emails/new_bid_email.html
Normal 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 %}
|
||||||
15
dta_service/core/templates/emails/password_change_email.html
Normal file
15
dta_service/core/templates/emails/password_change_email.html
Normal 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 %}
|
||||||
38
dta_service/core/templates/emails/password_reset_email.html
Normal file
38
dta_service/core/templates/emails/password_reset_email.html
Normal 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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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>
|
|
||||||
8
dta_service/core/urls/document.py
Normal file
8
dta_service/core/urls/document.py
Normal 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
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
dta_service/core/urls/public.py
Normal file
12
dta_service/core/urls/public.py
Normal 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",
|
||||||
|
),
|
||||||
|
]
|
||||||
12
dta_service/core/urls/support.py
Normal file
12
dta_service/core/urls/support.py
Normal 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)),
|
||||||
|
]
|
||||||
8
dta_service/core/urls/support_agent.py
Normal file
8
dta_service/core/urls/support_agent.py
Normal 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
17
dta_service/core/utils.py
Normal 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
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user