@@ -304,6 +304,14 @@ class VendorPicturesAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(SupportAgent)
|
@admin.register(SupportAgent)
|
||||||
class SupportAgentAdmin(admin.ModelAdmin):
|
class SupportAgentAdmin(admin.ModelAdmin):
|
||||||
model = SupportAgent
|
model = SupportAgent
|
||||||
|
list_display = ("user",)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
if obj is None:
|
||||||
|
from core.forms import SupportAgentCreationForm
|
||||||
|
|
||||||
|
kwargs["form"] = SupportAgentCreationForm
|
||||||
|
return super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(FAQ)
|
@admin.register(FAQ)
|
||||||
|
|||||||
36
dta_service/core/forms.py
Normal file
36
dta_service/core/forms.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from core.models.support_agent import SupportAgent
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class SupportAgentCreationForm(forms.ModelForm):
|
||||||
|
email = forms.EmailField(required=True)
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput, required=True)
|
||||||
|
first_name = forms.CharField(required=True)
|
||||||
|
last_name = forms.CharField(required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupportAgent
|
||||||
|
fields = ("email", "password", "first_name", "last_name")
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
email = self.cleaned_data["email"]
|
||||||
|
password = self.cleaned_data["password"]
|
||||||
|
first_name = self.cleaned_data["first_name"]
|
||||||
|
last_name = self.cleaned_data["last_name"]
|
||||||
|
|
||||||
|
user = User.objects.create_user(
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
user_type="support_agent",
|
||||||
|
)
|
||||||
|
|
||||||
|
support_agent = super().save(commit=False)
|
||||||
|
support_agent.user = user
|
||||||
|
if commit:
|
||||||
|
support_agent.save()
|
||||||
|
return support_agent
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-11-24 19:24
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0032_faq_supportagent_alter_user_user_type_supportcase_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
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"),
|
||||||
|
("attorney_engagement_letter", "Attorney Engagement Letter"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AttorneyEngagementLetter",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"document",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
primary_key=True,
|
||||||
|
related_name="attorney_engagement_letter_data",
|
||||||
|
serialize=False,
|
||||||
|
to="core.document",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("is_accepted", models.BooleanField(default=False)),
|
||||||
|
("accepted_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"attorney",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="engagement_letters",
|
||||||
|
to="core.attorney",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
45
dta_service/core/migrations/0034_create_default_attorney.py
Normal file
45
dta_service/core/migrations/0034_create_default_attorney.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
|
def create_default_attorney(apps, schema_editor):
|
||||||
|
User = apps.get_model('core', 'User')
|
||||||
|
Attorney = apps.get_model('core', 'Attorney')
|
||||||
|
|
||||||
|
# Create User
|
||||||
|
user, created = User.objects.get_or_create(
|
||||||
|
email="ryan@relawfirm",
|
||||||
|
defaults={
|
||||||
|
"password": make_password("asdfuweoriasdgn"),
|
||||||
|
"first_name": "Ryan",
|
||||||
|
"last_name": "Attorney",
|
||||||
|
"user_type": "attorney",
|
||||||
|
"is_active": True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
user.set_password("asdfuweoriasdgn")
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Create Attorney Profile
|
||||||
|
Attorney.objects.get_or_create(
|
||||||
|
user=user,
|
||||||
|
defaults={
|
||||||
|
"firm_name": "The Real Estate Law Firm, LLC",
|
||||||
|
"phone_number": "630-687-1070",
|
||||||
|
"address": "505 W Main St Suite A",
|
||||||
|
"city": "St. Charles",
|
||||||
|
"state": "IL",
|
||||||
|
"zip_code": "60174"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0033_alter_document_document_type_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_default_attorney),
|
||||||
|
]
|
||||||
@@ -1,797 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.contrib.auth.models import (
|
|
||||||
AbstractBaseUser,
|
|
||||||
BaseUserManager,
|
|
||||||
PermissionsMixin,
|
|
||||||
)
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.core.mail import send_mail
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
from django.conf import settings
|
|
||||||
import uuid
|
|
||||||
import os
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
|
||||||
def create_user(self, email, password=None, **extra_fields):
|
|
||||||
if not email:
|
|
||||||
raise ValueError("Users must have an email address")
|
|
||||||
|
|
||||||
email = self.normalize_email(email)
|
|
||||||
user = self.model(email=email, **extra_fields)
|
|
||||||
user.set_password(password)
|
|
||||||
user.save(using=self._db)
|
|
||||||
return user
|
|
||||||
|
|
||||||
def create_superuser(self, email, password=None, **extra_fields):
|
|
||||||
extra_fields.setdefault("is_staff", True)
|
|
||||||
extra_fields.setdefault("is_superuser", True)
|
|
||||||
return self.create_user(email, password, **extra_fields)
|
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractBaseUser, PermissionsMixin):
|
|
||||||
USER_TYPE_CHOICES = (
|
|
||||||
("property_owner", "Property Owner"),
|
|
||||||
("vendor", "Vendor"),
|
|
||||||
("attorney", "Attorney"),
|
|
||||||
("real_estate_agent", "Real Estate Agent"),
|
|
||||||
("support_agent", "Support Agent"),
|
|
||||||
("admin", "Admin"),
|
|
||||||
)
|
|
||||||
USER_TIER_CHOICES = (
|
|
||||||
("basic", "Basic"),
|
|
||||||
("premium", "Premium"),
|
|
||||||
("vendor", "Vendor"),
|
|
||||||
)
|
|
||||||
|
|
||||||
email = models.EmailField(unique=True)
|
|
||||||
first_name = models.CharField(max_length=30)
|
|
||||||
last_name = models.CharField(max_length=30)
|
|
||||||
user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES)
|
|
||||||
is_active = models.BooleanField(default=True)
|
|
||||||
is_staff = models.BooleanField(default=False)
|
|
||||||
date_joined = models.DateTimeField(default=timezone.now)
|
|
||||||
tos_signed = models.BooleanField(default=False)
|
|
||||||
profile_created = models.BooleanField(default=False)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
tier = models.CharField(max_length=20, choices=USER_TIER_CHOICES, default="basic")
|
|
||||||
|
|
||||||
objects = UserManager()
|
|
||||||
|
|
||||||
USERNAME_FIELD = "email"
|
|
||||||
REQUIRED_FIELDS = ["first_name", "last_name", "user_type"]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.email
|
|
||||||
|
|
||||||
def get_full_name(self):
|
|
||||||
return f"{self.first_name} {self.last_name}"
|
|
||||||
|
|
||||||
def get_short_name(self):
|
|
||||||
return self.first_name
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyOwner(models.Model):
|
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
|
||||||
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.user.get_full_name()
|
|
||||||
|
|
||||||
|
|
||||||
class Vendor(models.Model):
|
|
||||||
BUSINESS_TYPES = (
|
|
||||||
("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"),
|
|
||||||
)
|
|
||||||
|
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
|
||||||
business_name = models.CharField(max_length=100)
|
|
||||||
business_type = models.CharField(max_length=50, choices=BUSINESS_TYPES)
|
|
||||||
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
|
||||||
address = models.CharField(max_length=200)
|
|
||||||
city = models.CharField(max_length=100)
|
|
||||||
state = models.CharField(max_length=2)
|
|
||||||
zip_code = models.CharField(max_length=10)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
description = models.CharField(max_length=1024, default="")
|
|
||||||
website = models.URLField(blank=True, null=True)
|
|
||||||
services = models.JSONField(blank=True, default=list) # Changed to JSONField
|
|
||||||
service_areas = models.JSONField(blank=True, default=list) # Changed to JSONField
|
|
||||||
certifications = models.JSONField(
|
|
||||||
blank=True, null=True, default=list
|
|
||||||
) # Changed to JSONField
|
|
||||||
average_rating = models.DecimalField(
|
|
||||||
max_digits=3, decimal_places=2, blank=True, null=True
|
|
||||||
)
|
|
||||||
num_reviews = models.IntegerField(blank=True, null=True)
|
|
||||||
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
|
||||||
|
|
||||||
latitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
longitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
views = models.IntegerField(default=0)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.business_name
|
|
||||||
|
|
||||||
|
|
||||||
class Attorney(models.Model):
|
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
|
||||||
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
|
|
||||||
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
|
||||||
address = models.CharField(max_length=200)
|
|
||||||
city = models.CharField(max_length=100)
|
|
||||||
state = models.CharField(max_length=2)
|
|
||||||
zip_code = models.CharField(max_length=10)
|
|
||||||
specialties = models.JSONField(blank=True, default=list) # Store as JSON array
|
|
||||||
years_experience = models.IntegerField(default=0)
|
|
||||||
website = models.URLField(blank=True, null=True)
|
|
||||||
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
|
||||||
bio = models.TextField(blank=True, null=True) # Use TextField for longer text
|
|
||||||
licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
latitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
longitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.user.get_full_name()} ({self.firm_name})"
|
|
||||||
|
|
||||||
|
|
||||||
class RealEstateAgent(models.Model):
|
|
||||||
AGENT_TYPE_CHOICES = (
|
|
||||||
("buyer_agent", "Buyer's Agent"),
|
|
||||||
("seller_agent", "Seller's Agent"),
|
|
||||||
("dual_agent", "Dual Agent"),
|
|
||||||
("other", "Other"),
|
|
||||||
)
|
|
||||||
|
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
|
||||||
brokerage_name = models.CharField(max_length=200)
|
|
||||||
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)
|
|
||||||
address = models.CharField(max_length=200)
|
|
||||||
city = models.CharField(max_length=100)
|
|
||||||
state = models.CharField(max_length=2)
|
|
||||||
zip_code = models.CharField(max_length=10)
|
|
||||||
specialties = models.JSONField(blank=True, default=list) # Store as JSON array
|
|
||||||
years_experience = models.IntegerField(default=0)
|
|
||||||
website = models.URLField(blank=True, null=True)
|
|
||||||
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
|
||||||
bio = models.TextField(blank=True, null=True)
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
latitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
longitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
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):
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
|
|
||||||
class Property(models.Model):
|
|
||||||
PROPERTY_STATUS_TYPES = (
|
|
||||||
("active", "Active"),
|
|
||||||
("pending", "Pending"),
|
|
||||||
("contingent", "Contingent"),
|
|
||||||
("sold", "Sold"),
|
|
||||||
("off_market", "Off Market"),
|
|
||||||
)
|
|
||||||
owner = models.ForeignKey(
|
|
||||||
PropertyOwner, on_delete=models.CASCADE, related_name="properties"
|
|
||||||
)
|
|
||||||
address = models.CharField(max_length=200)
|
|
||||||
street = models.CharField(max_length=200, default="")
|
|
||||||
city = models.CharField(max_length=100)
|
|
||||||
state = models.CharField(max_length=2)
|
|
||||||
zip_code = models.CharField(max_length=10)
|
|
||||||
market_value = models.DecimalField(max_digits=12, decimal_places=2)
|
|
||||||
loan_amount = models.DecimalField(
|
|
||||||
max_digits=12, decimal_places=2, blank=True, null=True
|
|
||||||
)
|
|
||||||
loan_interest_rate = models.DecimalField(
|
|
||||||
max_digits=5, decimal_places=2, blank=True, null=True
|
|
||||||
)
|
|
||||||
loan_term = models.IntegerField(blank=True, null=True)
|
|
||||||
loan_start_date = models.DateField(blank=True, null=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
description = models.TextField(
|
|
||||||
blank=True, null=True
|
|
||||||
) # Text field for longer descriptions
|
|
||||||
sq_ft = models.IntegerField(blank=True, null=True) # Square footage
|
|
||||||
features = models.JSONField(blank=True, default=list) # Stores a list of strings
|
|
||||||
num_bedrooms = models.IntegerField(blank=True, null=True)
|
|
||||||
num_bathrooms = models.IntegerField(blank=True, null=True)
|
|
||||||
latitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
longitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
realestate_api_id = models.IntegerField()
|
|
||||||
|
|
||||||
property_status = models.CharField(
|
|
||||||
max_length=15, choices=PROPERTY_STATUS_TYPES, default="off_market"
|
|
||||||
)
|
|
||||||
listed_price = models.DecimalField(
|
|
||||||
max_digits=12, decimal_places=2, blank=True, null=True
|
|
||||||
)
|
|
||||||
views = models.IntegerField(default=0)
|
|
||||||
saves = models.IntegerField(default=0)
|
|
||||||
listed_date = models.DateTimeField(blank=True, null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.address}, {self.city}, {self.state} {self.zip_code}"
|
|
||||||
|
|
||||||
|
|
||||||
class SchoolInfo(models.Model):
|
|
||||||
SCHOOL_TYPES = (
|
|
||||||
("Public", "Public"),
|
|
||||||
("Other", "Other"),
|
|
||||||
)
|
|
||||||
property = models.ForeignKey(
|
|
||||||
Property,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="schools",
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
city = models.CharField(max_length=100)
|
|
||||||
state = models.CharField(max_length=2)
|
|
||||||
zip_code = models.CharField(max_length=10)
|
|
||||||
latitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
longitude = models.DecimalField(
|
|
||||||
max_digits=30, decimal_places=27, blank=True, null=True
|
|
||||||
) # For coordinates
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
enrollment = models.IntegerField()
|
|
||||||
grades = models.CharField(max_length=30)
|
|
||||||
name = models.CharField(max_length=256)
|
|
||||||
parent_rating = models.IntegerField()
|
|
||||||
rating = models.IntegerField()
|
|
||||||
school_type = models.CharField(
|
|
||||||
max_length=15, choices=SCHOOL_TYPES, default="public"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyTaxInfo(models.Model):
|
|
||||||
property = models.OneToOneField(
|
|
||||||
Property, on_delete=models.CASCADE, related_name="tax_info"
|
|
||||||
)
|
|
||||||
assessed_value = models.IntegerField()
|
|
||||||
assessment_year = models.IntegerField()
|
|
||||||
tax_amount = models.FloatField()
|
|
||||||
year = models.IntegerField()
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
|
|
||||||
class PropertySaleInfo(models.Model):
|
|
||||||
property = models.ForeignKey(
|
|
||||||
Property, on_delete=models.CASCADE, related_name="sale_info"
|
|
||||||
)
|
|
||||||
seq_no = models.IntegerField()
|
|
||||||
sale_date = models.DateTimeField()
|
|
||||||
sale_amount = models.FloatField()
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyWalkScoreInfo(models.Model):
|
|
||||||
property = models.OneToOneField(
|
|
||||||
Property,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
related_name="walk_score",
|
|
||||||
)
|
|
||||||
walk_score = models.IntegerField()
|
|
||||||
walk_description = models.CharField(max_length=256)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
ws_link = models.URLField()
|
|
||||||
logo_url = models.URLField()
|
|
||||||
transit_score = models.IntegerField()
|
|
||||||
transit_description = models.CharField(max_length=256)
|
|
||||||
transit_summary = models.CharField(max_length=512)
|
|
||||||
bike_score = models.IntegerField()
|
|
||||||
bike_description = models.CharField(max_length=256)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenHouse(models.Model):
|
|
||||||
property = models.ForeignKey(
|
|
||||||
Property,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
related_name="open_houses",
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
updated_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyPictures(models.Model):
|
|
||||||
Property = models.ForeignKey(
|
|
||||||
Property, on_delete=models.CASCADE, related_name="pictures"
|
|
||||||
)
|
|
||||||
image = models.FileField(upload_to="pictures/")
|
|
||||||
created_at = models.DateTimeField(auto_now_add=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):
|
|
||||||
name = models.CharField(max_length=100)
|
|
||||||
description = models.TextField(blank=True, null=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Video(models.Model):
|
|
||||||
category = models.ForeignKey(
|
|
||||||
VideoCategory, on_delete=models.CASCADE, related_name="videos"
|
|
||||||
)
|
|
||||||
title = models.CharField(max_length=200)
|
|
||||||
description = models.TextField(blank=True, null=True)
|
|
||||||
link = models.FileField(upload_to="videos/")
|
|
||||||
duration = models.IntegerField(help_text="Duration in seconds")
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
|
|
||||||
class UserVideoProgress(models.Model):
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
("not_started", "Not Started"),
|
|
||||||
("in_progress", "In Progress"),
|
|
||||||
("completed", "Completed"),
|
|
||||||
)
|
|
||||||
|
|
||||||
user = models.ForeignKey(
|
|
||||||
User, on_delete=models.CASCADE, related_name="video_progress"
|
|
||||||
)
|
|
||||||
video = models.ForeignKey(
|
|
||||||
Video, on_delete=models.CASCADE, related_name="user_progress"
|
|
||||||
)
|
|
||||||
progress = models.IntegerField(default=0, help_text="Progress in seconds")
|
|
||||||
status = models.CharField(
|
|
||||||
max_length=20, choices=STATUS_CHOICES, default="not_started"
|
|
||||||
)
|
|
||||||
last_watched = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ("user", "video")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.user.email} - {self.video.title} - {self.status}"
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# Update status based on progress
|
|
||||||
if self.progress == 0:
|
|
||||||
self.status = "not_started"
|
|
||||||
elif self.progress >= self.video.duration:
|
|
||||||
self.status = "completed"
|
|
||||||
self.progress = self.video.duration
|
|
||||||
else:
|
|
||||||
self.status = "in_progress"
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Conversation(models.Model):
|
|
||||||
property_owner = models.ForeignKey(
|
|
||||||
PropertyOwner, on_delete=models.CASCADE, related_name="conversations"
|
|
||||||
)
|
|
||||||
vendor = models.ForeignKey(
|
|
||||||
Vendor, on_delete=models.CASCADE, related_name="conversations"
|
|
||||||
)
|
|
||||||
property = models.ForeignKey(
|
|
||||||
Property,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="conversations",
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
# class Meta:
|
|
||||||
# unique_together = ('property_owner', 'vendor', 'property')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Conversation between {self.property_owner} and {self.vendor} about {self.property}"
|
|
||||||
|
|
||||||
|
|
||||||
def message_file_path(instance, filename):
|
|
||||||
ext = filename.split(".")[-1]
|
|
||||||
filename = f"{uuid.uuid4()}.{ext}"
|
|
||||||
return os.path.join("message_attachments", filename)
|
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
|
||||||
conversation = models.ForeignKey(
|
|
||||||
Conversation, on_delete=models.CASCADE, related_name="messages"
|
|
||||||
)
|
|
||||||
sender = models.ForeignKey(
|
|
||||||
User, on_delete=models.CASCADE, related_name="sent_messages"
|
|
||||||
)
|
|
||||||
text = models.TextField()
|
|
||||||
attachment = models.FileField(upload_to=message_file_path, blank=True, null=True)
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
|
||||||
read = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Message from {self.sender} in {self.conversation}"
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetToken(models.Model):
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
||||||
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
expires_at = models.DateTimeField()
|
|
||||||
used = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
return not self.used and self.expires_at > timezone.now()
|
|
||||||
|
|
||||||
def send_reset_email(self):
|
|
||||||
subject = "Password Reset Request"
|
|
||||||
reset_url = f"{settings.FRONTEND_URL}/reset-password/{self.token}/"
|
|
||||||
context = {
|
|
||||||
"user": self.user,
|
|
||||||
"reset_url": reset_url,
|
|
||||||
}
|
|
||||||
html_message = render_to_string("password_reset_email.html", context)
|
|
||||||
plain_message = strip_tags(html_message)
|
|
||||||
send_mail(
|
|
||||||
subject,
|
|
||||||
plain_message,
|
|
||||||
settings.DEFAULT_FROM_EMAIL,
|
|
||||||
[self.user.email],
|
|
||||||
html_message=html_message,
|
|
||||||
fail_silently=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Password reset token for {self.user.email}"
|
|
||||||
|
|
||||||
|
|
||||||
# class Offer(models.Model):
|
|
||||||
# OFFER_STATUS_TYPES = (
|
|
||||||
# ("submitted", "Submitted"),
|
|
||||||
# ("draft", "Draft"),
|
|
||||||
# ("accepted", "Accepted"),
|
|
||||||
# ("rejected", "Rejected"),
|
|
||||||
# ("counter", "Counter"),
|
|
||||||
# ("withdrawn", "Withdrawn"),
|
|
||||||
# )
|
|
||||||
# user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
||||||
# property = models.ForeignKey(Property, on_delete=models.PROTECT)
|
|
||||||
# status = models.CharField(
|
|
||||||
# max_length=10, choices=OFFER_STATUS_TYPES, default="draft"
|
|
||||||
# )
|
|
||||||
# previous_offer = models.ForeignKey(
|
|
||||||
# "self", on_delete=models.CASCADE, null=True, blank=True
|
|
||||||
# )
|
|
||||||
# is_active = models.BooleanField(default=True)
|
|
||||||
# created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
# updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"{self.user.email} {self.status} {self.property.address}"
|
|
||||||
|
|
||||||
|
|
||||||
class Bid(models.Model):
|
|
||||||
BID_TYPE_CHOICES = (
|
|
||||||
("electrical", "Electrical"),
|
|
||||||
("plumbing", "Plumbing"),
|
|
||||||
("carpentry", "Carpentry"),
|
|
||||||
("general_contractor", "General Contractor"),
|
|
||||||
)
|
|
||||||
LOCATION_CHOICES = (
|
|
||||||
("living_room", "Living Room"),
|
|
||||||
("basement", "Basement"),
|
|
||||||
("kitchen", "Kitchen"),
|
|
||||||
("bathroom", "Bathroom"),
|
|
||||||
("bedroom", "Bedroom"),
|
|
||||||
("outside", "Outside"),
|
|
||||||
)
|
|
||||||
|
|
||||||
property = models.ForeignKey(
|
|
||||||
Property, on_delete=models.CASCADE, related_name="bids"
|
|
||||||
)
|
|
||||||
description = models.TextField()
|
|
||||||
bid_type = models.CharField(max_length=50, choices=BID_TYPE_CHOICES)
|
|
||||||
location = models.CharField(max_length=50, choices=LOCATION_CHOICES)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Bid for {self.bid_type} at {self.property.address}"
|
|
||||||
|
|
||||||
|
|
||||||
class BidImage(models.Model):
|
|
||||||
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="images")
|
|
||||||
image = models.FileField(upload_to="bid_pictures/")
|
|
||||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
|
|
||||||
class BidResponse(models.Model):
|
|
||||||
RESPONSE_STATUS_CHOICES = (
|
|
||||||
("draft", "Draft"),
|
|
||||||
("submitted", "Submitted"),
|
|
||||||
("selected", "Selected"),
|
|
||||||
)
|
|
||||||
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="responses")
|
|
||||||
vendor = models.ForeignKey(
|
|
||||||
Vendor, on_delete=models.CASCADE, related_name="bid_responses"
|
|
||||||
)
|
|
||||||
description = models.TextField()
|
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
|
||||||
status = models.CharField(
|
|
||||||
max_length=20, choices=RESPONSE_STATUS_CHOICES, default="draft"
|
|
||||||
)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ("bid", "vendor")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Response from {self.vendor.business_name} for Bid {self.bid.id}"
|
|
||||||
|
|
||||||
|
|
||||||
class PropertySave(models.Model):
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
||||||
property = models.ForeignKey(Property, on_delete=models.CASCADE)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
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)
|
|
||||||
13
dta_service/core/models/__init__.py
Normal file
13
dta_service/core/models/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from .user import UserManager, User, UserViewModel, PasswordResetToken
|
||||||
|
from .property_owner import PropertyOwner
|
||||||
|
from .vendor import Vendor, VendorPictures
|
||||||
|
from .attorney import Attorney
|
||||||
|
from .real_estate_agent import RealEstateAgent
|
||||||
|
from .support_agent import SupportAgent
|
||||||
|
from .property import Property, PropertyPictures, PropertySave
|
||||||
|
from .property_info import SchoolInfo, PropertyTaxInfo, PropertySaleInfo, PropertyWalkScoreInfo, OpenHouse
|
||||||
|
from .video import VideoCategory, Video, UserVideoProgress
|
||||||
|
from .conversation import Conversation, Message, message_file_path
|
||||||
|
from .bid import Bid, BidImage, BidResponse
|
||||||
|
from .document import Document, OfferDocument, SellerDisclosure, HomeImprovementReceipt, AttorneyEngagementLetter, LendorFinancingAgreement
|
||||||
|
from .support import FAQ, SupportCase, SupportMessage
|
||||||
33
dta_service/core/models/attorney.py
Normal file
33
dta_service/core/models/attorney.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Attorney(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
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
|
||||||
|
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
address = models.CharField(max_length=200)
|
||||||
|
city = models.CharField(max_length=100)
|
||||||
|
state = models.CharField(max_length=2)
|
||||||
|
zip_code = models.CharField(max_length=10)
|
||||||
|
specialties = models.JSONField(blank=True, default=list) # Store as JSON array
|
||||||
|
years_experience = models.IntegerField(default=0)
|
||||||
|
website = models.URLField(blank=True, null=True)
|
||||||
|
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
||||||
|
bio = models.TextField(blank=True, null=True) # Use TextField for longer text
|
||||||
|
licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.get_full_name()} ({self.firm_name})"
|
||||||
63
dta_service/core/models/bid.py
Normal file
63
dta_service/core/models/bid.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .property import Property
|
||||||
|
from .vendor import Vendor
|
||||||
|
|
||||||
|
|
||||||
|
class Bid(models.Model):
|
||||||
|
BID_TYPE_CHOICES = (
|
||||||
|
("electrical", "Electrical"),
|
||||||
|
("plumbing", "Plumbing"),
|
||||||
|
("carpentry", "Carpentry"),
|
||||||
|
("general_contractor", "General Contractor"),
|
||||||
|
)
|
||||||
|
LOCATION_CHOICES = (
|
||||||
|
("living_room", "Living Room"),
|
||||||
|
("basement", "Basement"),
|
||||||
|
("kitchen", "Kitchen"),
|
||||||
|
("bathroom", "Bathroom"),
|
||||||
|
("bedroom", "Bedroom"),
|
||||||
|
("outside", "Outside"),
|
||||||
|
)
|
||||||
|
|
||||||
|
property = models.ForeignKey(
|
||||||
|
Property, on_delete=models.CASCADE, related_name="bids"
|
||||||
|
)
|
||||||
|
description = models.TextField()
|
||||||
|
bid_type = models.CharField(max_length=50, choices=BID_TYPE_CHOICES)
|
||||||
|
location = models.CharField(max_length=50, choices=LOCATION_CHOICES)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Bid for {self.bid_type} at {self.property.address}"
|
||||||
|
|
||||||
|
|
||||||
|
class BidImage(models.Model):
|
||||||
|
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="images")
|
||||||
|
image = models.FileField(upload_to="bid_pictures/")
|
||||||
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BidResponse(models.Model):
|
||||||
|
RESPONSE_STATUS_CHOICES = (
|
||||||
|
("draft", "Draft"),
|
||||||
|
("submitted", "Submitted"),
|
||||||
|
("selected", "Selected"),
|
||||||
|
)
|
||||||
|
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="responses")
|
||||||
|
vendor = models.ForeignKey(
|
||||||
|
Vendor, on_delete=models.CASCADE, related_name="bid_responses"
|
||||||
|
)
|
||||||
|
description = models.TextField()
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20, choices=RESPONSE_STATUS_CHOICES, default="draft"
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("bid", "vendor")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Response from {self.vendor.business_name} for Bid {self.bid.id}"
|
||||||
53
dta_service/core/models/conversation.py
Normal file
53
dta_service/core/models/conversation.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .property_owner import PropertyOwner
|
||||||
|
from .vendor import Vendor
|
||||||
|
from .property import Property
|
||||||
|
from .user import User
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Conversation(models.Model):
|
||||||
|
property_owner = models.ForeignKey(
|
||||||
|
PropertyOwner, on_delete=models.CASCADE, related_name="conversations"
|
||||||
|
)
|
||||||
|
vendor = models.ForeignKey(
|
||||||
|
Vendor, on_delete=models.CASCADE, related_name="conversations"
|
||||||
|
)
|
||||||
|
property = models.ForeignKey(
|
||||||
|
Property,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="conversations",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
# class Meta:
|
||||||
|
# unique_together = ('property_owner', 'vendor', 'property')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Conversation between {self.property_owner} and {self.vendor} about {self.property}"
|
||||||
|
|
||||||
|
|
||||||
|
def message_file_path(instance, filename):
|
||||||
|
ext = filename.split(".")[-1]
|
||||||
|
filename = f"{uuid.uuid4()}.{ext}"
|
||||||
|
return os.path.join("message_attachments", filename)
|
||||||
|
|
||||||
|
|
||||||
|
class Message(models.Model):
|
||||||
|
conversation = models.ForeignKey(
|
||||||
|
Conversation, on_delete=models.CASCADE, related_name="messages"
|
||||||
|
)
|
||||||
|
sender = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, related_name="sent_messages"
|
||||||
|
)
|
||||||
|
text = models.TextField()
|
||||||
|
attachment = models.FileField(upload_to=message_file_path, blank=True, null=True)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
read = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Message from {self.sender} in {self.conversation}"
|
||||||
166
dta_service/core/models/document.py
Normal file
166
dta_service/core/models/document.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .property import Property
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
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"),
|
||||||
|
("attorney_engagement_letter", "Attorney Engagement Letter"),
|
||||||
|
("lendor_financing_agreement", "Lendor Financing Agreement"),
|
||||||
|
("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 AttorneyEngagementLetter(models.Model):
|
||||||
|
# One-to-one link to the generic Document
|
||||||
|
document = models.OneToOneField(
|
||||||
|
Document,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="attorney_engagement_letter_data",
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
attorney = models.ForeignKey(
|
||||||
|
"core.Attorney", on_delete=models.SET_NULL, null=True, related_name="engagement_letters"
|
||||||
|
)
|
||||||
|
is_accepted = models.BooleanField(default=False)
|
||||||
|
accepted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Attorney Engagement Letter for Document ID: {self.document.id}"
|
||||||
|
|
||||||
|
|
||||||
|
class LendorFinancingAgreement(models.Model):
|
||||||
|
document = models.OneToOneField(
|
||||||
|
Document,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="lendor_financing_agreement_data",
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
offer = models.ForeignKey(
|
||||||
|
OfferDocument,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="financing_agreements",
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
is_signed = models.BooleanField(default=False)
|
||||||
|
date_signed = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Lendor Financing Agreement for Document ID: {self.document.id}"
|
||||||
78
dta_service/core/models/property.py
Normal file
78
dta_service/core/models/property.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .property_owner import PropertyOwner
|
||||||
|
from .user import User
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Property(models.Model):
|
||||||
|
PROPERTY_STATUS_TYPES = (
|
||||||
|
("active", "Active"),
|
||||||
|
("pending", "Pending"),
|
||||||
|
("contingent", "Contingent"),
|
||||||
|
("sold", "Sold"),
|
||||||
|
("off_market", "Off Market"),
|
||||||
|
)
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
PropertyOwner, on_delete=models.CASCADE, related_name="properties"
|
||||||
|
)
|
||||||
|
address = models.CharField(max_length=200)
|
||||||
|
street = models.CharField(max_length=200, default="")
|
||||||
|
city = models.CharField(max_length=100)
|
||||||
|
state = models.CharField(max_length=2)
|
||||||
|
zip_code = models.CharField(max_length=10)
|
||||||
|
market_value = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
|
loan_amount = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, blank=True, null=True
|
||||||
|
)
|
||||||
|
loan_interest_rate = models.DecimalField(
|
||||||
|
max_digits=5, decimal_places=2, blank=True, null=True
|
||||||
|
)
|
||||||
|
loan_term = models.IntegerField(blank=True, null=True)
|
||||||
|
loan_start_date = models.DateField(blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
description = models.TextField(
|
||||||
|
blank=True, null=True
|
||||||
|
) # Text field for longer descriptions
|
||||||
|
sq_ft = models.IntegerField(blank=True, null=True) # Square footage
|
||||||
|
features = models.JSONField(blank=True, default=list) # Stores a list of strings
|
||||||
|
num_bedrooms = models.IntegerField(blank=True, null=True)
|
||||||
|
num_bathrooms = models.IntegerField(blank=True, null=True)
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
realestate_api_id = models.IntegerField()
|
||||||
|
|
||||||
|
property_status = models.CharField(
|
||||||
|
max_length=15, choices=PROPERTY_STATUS_TYPES, default="off_market"
|
||||||
|
)
|
||||||
|
listed_price = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, blank=True, null=True
|
||||||
|
)
|
||||||
|
views = models.IntegerField(default=0)
|
||||||
|
saves = models.IntegerField(default=0)
|
||||||
|
listed_date = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.address}, {self.city}, {self.state} {self.zip_code}"
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyPictures(models.Model):
|
||||||
|
Property = models.ForeignKey(
|
||||||
|
Property, on_delete=models.CASCADE, related_name="pictures"
|
||||||
|
)
|
||||||
|
image = models.FileField(upload_to="pictures/")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertySave(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
property = models.ForeignKey(Property, on_delete=models.CASCADE)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("user", "property")
|
||||||
95
dta_service/core/models/property_info.py
Normal file
95
dta_service/core/models/property_info.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .property import Property
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SchoolInfo(models.Model):
|
||||||
|
SCHOOL_TYPES = (
|
||||||
|
("Public", "Public"),
|
||||||
|
("Other", "Other"),
|
||||||
|
)
|
||||||
|
property = models.ForeignKey(
|
||||||
|
Property,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="schools",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
city = models.CharField(max_length=100)
|
||||||
|
state = models.CharField(max_length=2)
|
||||||
|
zip_code = models.CharField(max_length=10)
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
enrollment = models.IntegerField()
|
||||||
|
grades = models.CharField(max_length=30)
|
||||||
|
name = models.CharField(max_length=256)
|
||||||
|
parent_rating = models.IntegerField()
|
||||||
|
rating = models.IntegerField()
|
||||||
|
school_type = models.CharField(
|
||||||
|
max_length=15, choices=SCHOOL_TYPES, default="public"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyTaxInfo(models.Model):
|
||||||
|
property = models.OneToOneField(
|
||||||
|
Property, on_delete=models.CASCADE, related_name="tax_info"
|
||||||
|
)
|
||||||
|
assessed_value = models.IntegerField()
|
||||||
|
assessment_year = models.IntegerField()
|
||||||
|
tax_amount = models.FloatField()
|
||||||
|
year = models.IntegerField()
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertySaleInfo(models.Model):
|
||||||
|
property = models.ForeignKey(
|
||||||
|
Property, on_delete=models.CASCADE, related_name="sale_info"
|
||||||
|
)
|
||||||
|
seq_no = models.IntegerField()
|
||||||
|
sale_date = models.DateTimeField()
|
||||||
|
sale_amount = models.FloatField()
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyWalkScoreInfo(models.Model):
|
||||||
|
property = models.OneToOneField(
|
||||||
|
Property,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="walk_score",
|
||||||
|
)
|
||||||
|
walk_score = models.IntegerField()
|
||||||
|
walk_description = models.CharField(max_length=256)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
ws_link = models.URLField()
|
||||||
|
logo_url = models.URLField()
|
||||||
|
transit_score = models.IntegerField()
|
||||||
|
transit_description = models.CharField(max_length=256)
|
||||||
|
transit_summary = models.CharField(max_length=512)
|
||||||
|
bike_score = models.IntegerField()
|
||||||
|
bike_description = models.CharField(max_length=256)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenHouse(models.Model):
|
||||||
|
property = models.ForeignKey(
|
||||||
|
Property,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="open_houses",
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
updated_at = models.DateTimeField(auto_now_add=True)
|
||||||
12
dta_service/core/models/property_owner.py
Normal file
12
dta_service/core/models/property_owner.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyOwner(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user.get_full_name()
|
||||||
43
dta_service/core/models/real_estate_agent.py
Normal file
43
dta_service/core/models/real_estate_agent.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class RealEstateAgent(models.Model):
|
||||||
|
AGENT_TYPE_CHOICES = (
|
||||||
|
("buyer_agent", "Buyer's Agent"),
|
||||||
|
("seller_agent", "Seller's Agent"),
|
||||||
|
("dual_agent", "Dual Agent"),
|
||||||
|
("other", "Other"),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
brokerage_name = models.CharField(max_length=200)
|
||||||
|
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)
|
||||||
|
address = models.CharField(max_length=200)
|
||||||
|
city = models.CharField(max_length=100)
|
||||||
|
state = models.CharField(max_length=2)
|
||||||
|
zip_code = models.CharField(max_length=10)
|
||||||
|
specialties = models.JSONField(blank=True, default=list) # Store as JSON array
|
||||||
|
years_experience = models.IntegerField(default=0)
|
||||||
|
website = models.URLField(blank=True, null=True)
|
||||||
|
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
||||||
|
bio = models.TextField(blank=True, null=True)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.get_full_name()} ({self.brokerage_name})"
|
||||||
42
dta_service/core/models/support.py
Normal file
42
dta_service/core/models/support.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
6
dta_service/core/models/support_agent.py
Normal file
6
dta_service/core/models/support_agent.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class SupportAgent(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
109
dta_service/core/models/user.py
Normal file
109
dta_service/core/models/user.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import (
|
||||||
|
AbstractBaseUser,
|
||||||
|
BaseUserManager,
|
||||||
|
PermissionsMixin,
|
||||||
|
)
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
from django.conf import settings
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(BaseUserManager):
|
||||||
|
def create_user(self, email, password=None, **extra_fields):
|
||||||
|
if not email:
|
||||||
|
raise ValueError("Users must have an email address")
|
||||||
|
|
||||||
|
email = self.normalize_email(email)
|
||||||
|
user = self.model(email=email, **extra_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_superuser(self, email, password=None, **extra_fields):
|
||||||
|
extra_fields.setdefault("is_staff", True)
|
||||||
|
extra_fields.setdefault("is_superuser", True)
|
||||||
|
return self.create_user(email, password, **extra_fields)
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractBaseUser, PermissionsMixin):
|
||||||
|
USER_TYPE_CHOICES = (
|
||||||
|
("property_owner", "Property Owner"),
|
||||||
|
("vendor", "Vendor"),
|
||||||
|
("attorney", "Attorney"),
|
||||||
|
("real_estate_agent", "Real Estate Agent"),
|
||||||
|
("support_agent", "Support Agent"),
|
||||||
|
("admin", "Admin"),
|
||||||
|
)
|
||||||
|
USER_TIER_CHOICES = (
|
||||||
|
("basic", "Basic"),
|
||||||
|
("premium", "Premium"),
|
||||||
|
("vendor", "Vendor"),
|
||||||
|
)
|
||||||
|
|
||||||
|
email = models.EmailField(unique=True)
|
||||||
|
first_name = models.CharField(max_length=30)
|
||||||
|
last_name = models.CharField(max_length=30)
|
||||||
|
user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
is_staff = models.BooleanField(default=False)
|
||||||
|
date_joined = models.DateTimeField(default=timezone.now)
|
||||||
|
tos_signed = models.BooleanField(default=False)
|
||||||
|
profile_created = models.BooleanField(default=False)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
tier = models.CharField(max_length=20, choices=USER_TIER_CHOICES, default="basic")
|
||||||
|
|
||||||
|
objects = UserManager()
|
||||||
|
|
||||||
|
USERNAME_FIELD = "email"
|
||||||
|
REQUIRED_FIELDS = ["first_name", "last_name", "user_type"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
|
def get_full_name(self):
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
def get_short_name(self):
|
||||||
|
return self.first_name
|
||||||
|
|
||||||
|
|
||||||
|
class UserViewModel(models.Model):
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetToken(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
expires_at = models.DateTimeField()
|
||||||
|
used = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return not self.used and self.expires_at > timezone.now()
|
||||||
|
|
||||||
|
def send_reset_email(self):
|
||||||
|
subject = "Password Reset Request"
|
||||||
|
reset_url = f"{settings.FRONTEND_URL}/reset-password/{self.token}/"
|
||||||
|
context = {
|
||||||
|
"user": self.user,
|
||||||
|
"reset_url": reset_url,
|
||||||
|
}
|
||||||
|
html_message = render_to_string("password_reset_email.html", context)
|
||||||
|
plain_message = strip_tags(html_message)
|
||||||
|
send_mail(
|
||||||
|
subject,
|
||||||
|
plain_message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[self.user.email],
|
||||||
|
html_message=html_message,
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Password reset token for {self.user.email}"
|
||||||
81
dta_service/core/models/vendor.py
Normal file
81
dta_service/core/models/vendor.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Vendor(models.Model):
|
||||||
|
BUSINESS_TYPES = (
|
||||||
|
("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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
business_name = models.CharField(max_length=100)
|
||||||
|
business_type = models.CharField(max_length=50, choices=BUSINESS_TYPES)
|
||||||
|
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
address = models.CharField(max_length=200)
|
||||||
|
city = models.CharField(max_length=100)
|
||||||
|
state = models.CharField(max_length=2)
|
||||||
|
zip_code = models.CharField(max_length=10)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
description = models.CharField(max_length=1024, default="")
|
||||||
|
website = models.URLField(blank=True, null=True)
|
||||||
|
services = models.JSONField(blank=True, default=list) # Changed to JSONField
|
||||||
|
service_areas = models.JSONField(blank=True, default=list) # Changed to JSONField
|
||||||
|
certifications = models.JSONField(
|
||||||
|
blank=True, null=True, default=list
|
||||||
|
) # Changed to JSONField
|
||||||
|
average_rating = models.DecimalField(
|
||||||
|
max_digits=3, decimal_places=2, blank=True, null=True
|
||||||
|
)
|
||||||
|
num_reviews = models.IntegerField(blank=True, null=True)
|
||||||
|
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
||||||
|
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=30, decimal_places=27, blank=True, null=True
|
||||||
|
) # For coordinates
|
||||||
|
views = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.business_name
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
64
dta_service/core/models/video.py
Normal file
64
dta_service/core/models/video.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from django.db import models
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class VideoCategory(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Video(models.Model):
|
||||||
|
category = models.ForeignKey(
|
||||||
|
VideoCategory, on_delete=models.CASCADE, related_name="videos"
|
||||||
|
)
|
||||||
|
title = models.CharField(max_length=200)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
link = models.FileField(upload_to="videos/")
|
||||||
|
duration = models.IntegerField(help_text="Duration in seconds")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
class UserVideoProgress(models.Model):
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
("not_started", "Not Started"),
|
||||||
|
("in_progress", "In Progress"),
|
||||||
|
("completed", "Completed"),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, related_name="video_progress"
|
||||||
|
)
|
||||||
|
video = models.ForeignKey(
|
||||||
|
Video, on_delete=models.CASCADE, related_name="user_progress"
|
||||||
|
)
|
||||||
|
progress = models.IntegerField(default=0, help_text="Progress in seconds")
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20, choices=STATUS_CHOICES, default="not_started"
|
||||||
|
)
|
||||||
|
last_watched = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("user", "video")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.email} - {self.video.title} - {self.status}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Update status based on progress
|
||||||
|
if self.progress == 0:
|
||||||
|
self.status = "not_started"
|
||||||
|
elif self.progress >= self.video.duration:
|
||||||
|
self.status = "completed"
|
||||||
|
self.progress = self.video.duration
|
||||||
|
else:
|
||||||
|
self.status = "in_progress"
|
||||||
|
super().save(*args, **kwargs)
|
||||||
File diff suppressed because it is too large
Load Diff
54
dta_service/core/serializers/__init__.py
Normal file
54
dta_service/core/serializers/__init__.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from .user import (
|
||||||
|
CustomTokenObtainPairSerializer,
|
||||||
|
UserSerializer,
|
||||||
|
UserRegisterSerializer,
|
||||||
|
PasswordResetRequestSerializer,
|
||||||
|
PasswordResetConfirmSerializer,
|
||||||
|
)
|
||||||
|
from .property_owner import PropertyOwnerSerializer
|
||||||
|
from .vendor import VendorSerializer
|
||||||
|
from .attorney import AttorneySerializer
|
||||||
|
from .real_estate_agent import RealEstateAgentSerializer
|
||||||
|
from .support_agent import SupportAgentSerializer
|
||||||
|
from .property import (
|
||||||
|
PropertyPictureSerializer,
|
||||||
|
PublicPropertyResponseSerializer,
|
||||||
|
PropertyResponseSerializer,
|
||||||
|
PropertyRequestSerializer,
|
||||||
|
PropertySaveSerializer,
|
||||||
|
)
|
||||||
|
from .property_info import (
|
||||||
|
SchoolInfoSerializer,
|
||||||
|
PropertyWalkScoreInfoSerializer,
|
||||||
|
PropertyTaxInfoSerializer,
|
||||||
|
PropertySaleInfoSerializer,
|
||||||
|
OpenHouseSerializer,
|
||||||
|
)
|
||||||
|
from .video import (
|
||||||
|
VideoCategorySerializer,
|
||||||
|
VideoSerializer,
|
||||||
|
UserVideoProgressSerializer,
|
||||||
|
)
|
||||||
|
from .conversation import (
|
||||||
|
MessageSerializer,
|
||||||
|
ConversationResponseSerializer,
|
||||||
|
ConversationRequestSerializer,
|
||||||
|
)
|
||||||
|
from .bid import (
|
||||||
|
BidImageSerializer,
|
||||||
|
BidResponseSerializer,
|
||||||
|
BidSerializer,
|
||||||
|
)
|
||||||
|
from .document import (
|
||||||
|
OfferSerializer,
|
||||||
|
SellerDisclosureSerializer,
|
||||||
|
HomeImprovementReceiptSerializer,
|
||||||
|
DocumentSerializer,
|
||||||
|
LendorFinancingAgreementSerializer,
|
||||||
|
)
|
||||||
|
from .support import (
|
||||||
|
SupportMessageSerializer,
|
||||||
|
SupportCaseListSerializer,
|
||||||
|
SupportCaseDetailSerializer,
|
||||||
|
FAQSerializer,
|
||||||
|
)
|
||||||
79
dta_service/core/serializers/attorney.py
Normal file
79
dta_service/core/serializers/attorney.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import Attorney
|
||||||
|
from .user import UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class AttorneySerializer(serializers.ModelSerializer):
|
||||||
|
user = UserSerializer(
|
||||||
|
read_only=True
|
||||||
|
) # Nested serializer for the related User object
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Attorney
|
||||||
|
fields = [
|
||||||
|
"user",
|
||||||
|
"firm_name",
|
||||||
|
"phone_number",
|
||||||
|
"address",
|
||||||
|
"city",
|
||||||
|
"state",
|
||||||
|
"zip_code",
|
||||||
|
"specialties",
|
||||||
|
"years_experience",
|
||||||
|
"website",
|
||||||
|
"profile_picture",
|
||||||
|
"bio",
|
||||||
|
"licensed_states",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"longitude",
|
||||||
|
"latitude",
|
||||||
|
]
|
||||||
|
read_only_fields = ["created_at", "updated_at"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# 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.
|
||||||
|
# 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.
|
||||||
|
user_instance = self.context.get("user")
|
||||||
|
if not user_instance:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"User instance must be provided to create an Attorney."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the user_type is correctly set for the new user
|
||||||
|
if user_instance.user_type != "attorney":
|
||||||
|
user_instance.user_type = "attorney"
|
||||||
|
user_instance.save()
|
||||||
|
|
||||||
|
attorney = Attorney.objects.create(user=user_instance, **validated_data)
|
||||||
|
return attorney
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# Handle updates for Attorney fields
|
||||||
|
instance.firm_name = validated_data.get("firm_name", instance.firm_name)
|
||||||
|
instance.bar_number = validated_data.get("bar_number", instance.bar_number)
|
||||||
|
instance.phone_number = validated_data.get(
|
||||||
|
"phone_number", instance.phone_number
|
||||||
|
)
|
||||||
|
instance.address = validated_data.get("address", instance.address)
|
||||||
|
instance.city = validated_data.get("city", instance.city)
|
||||||
|
instance.state = validated_data.get("state", instance.state)
|
||||||
|
instance.zip_code = validated_data.get("zip_code", instance.zip_code)
|
||||||
|
instance.specialties = validated_data.get("specialties", instance.specialties)
|
||||||
|
instance.years_experience = validated_data.get(
|
||||||
|
"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.longitude = validated_data.get("longitude", instance.longitude)
|
||||||
|
instance.latitude = validated_data.get("latitude", instance.latitude)
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
55
dta_service/core/serializers/bid.py
Normal file
55
dta_service/core/serializers/bid.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import Bid, BidImage, BidResponse
|
||||||
|
from .vendor import VendorSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BidImageSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BidImage
|
||||||
|
fields = ["id", "image"]
|
||||||
|
|
||||||
|
|
||||||
|
class BidResponseSerializer(serializers.ModelSerializer):
|
||||||
|
vendor = VendorSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BidResponse
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"bid",
|
||||||
|
"vendor",
|
||||||
|
"description",
|
||||||
|
"price",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at", "vendor"]
|
||||||
|
|
||||||
|
|
||||||
|
class BidSerializer(serializers.ModelSerializer):
|
||||||
|
images = BidImageSerializer(many=True, read_only=True)
|
||||||
|
responses = BidResponseSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Bid
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"property",
|
||||||
|
"description",
|
||||||
|
"bid_type",
|
||||||
|
"location",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"images",
|
||||||
|
"responses",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at", "responses"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
images_data = self.context.get("request").FILES.getlist("images")
|
||||||
|
bid = Bid.objects.create(**validated_data)
|
||||||
|
for image_data in images_data:
|
||||||
|
# Assuming you have an image upload logic, like storing to S3 and getting a URL
|
||||||
|
BidImage.objects.create(bid=bid, image=image_data)
|
||||||
|
return bid
|
||||||
84
dta_service/core/serializers/conversation.py
Normal file
84
dta_service/core/serializers/conversation.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import Conversation, Message
|
||||||
|
from .vendor import VendorSerializer
|
||||||
|
from .property_owner import PropertyOwnerSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class MessageSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Message
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"conversation",
|
||||||
|
"sender",
|
||||||
|
"text",
|
||||||
|
"attachment",
|
||||||
|
"timestamp",
|
||||||
|
"read",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "timestamp"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Extract user data
|
||||||
|
sender = validated_data.pop("sender")
|
||||||
|
|
||||||
|
message = Message.objects.create(sender=sender, **validated_data)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
"""
|
||||||
|
Handle updates to message fields.
|
||||||
|
Note: sender and conversation are typically read-only in updates
|
||||||
|
"""
|
||||||
|
# Update text if provided
|
||||||
|
instance.text = validated_data.get("text", instance.text)
|
||||||
|
|
||||||
|
# Update read status if provided
|
||||||
|
if "read" in validated_data:
|
||||||
|
instance.read = validated_data["read"]
|
||||||
|
|
||||||
|
# Handle attachment updates (if needed)
|
||||||
|
if "attachment" in validated_data:
|
||||||
|
# Delete old attachment if exists
|
||||||
|
if instance.attachment:
|
||||||
|
instance.attachment.delete()
|
||||||
|
instance.attachment = validated_data["attachment"]
|
||||||
|
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationResponseSerializer(serializers.ModelSerializer):
|
||||||
|
vendor = VendorSerializer()
|
||||||
|
property_owner = PropertyOwnerSerializer()
|
||||||
|
messages = MessageSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Conversation
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"property_owner",
|
||||||
|
"vendor",
|
||||||
|
"property",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"messages",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationRequestSerializer(serializers.ModelSerializer):
|
||||||
|
messages = MessageSerializer(many=True, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Conversation
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"property_owner",
|
||||||
|
"vendor",
|
||||||
|
"property",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"messages",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
115
dta_service/core/serializers/document.py
Normal file
115
dta_service/core/serializers/document.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import (
|
||||||
|
Document,
|
||||||
|
OfferDocument,
|
||||||
|
SellerDisclosure,
|
||||||
|
HomeImprovementReceipt,
|
||||||
|
AttorneyEngagementLetter,
|
||||||
|
LendorFinancingAgreement,
|
||||||
|
User,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 AttorneyEngagementLetterSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = AttorneyEngagementLetter
|
||||||
|
exclude = ["document"]
|
||||||
|
|
||||||
|
|
||||||
|
class LendorFinancingAgreementSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = LendorFinancingAgreement
|
||||||
|
exclude = ["document"]
|
||||||
|
|
||||||
|
|
||||||
|
class SubDocumentField(serializers.RelatedField):
|
||||||
|
def to_representation(self, value):
|
||||||
|
# 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
|
||||||
|
elif hasattr(value, "attorney_engagement_letter_data"):
|
||||||
|
return AttorneyEngagementLetterSerializer(
|
||||||
|
value.attorney_engagement_letter_data
|
||||||
|
).data
|
||||||
|
elif hasattr(value, "lendor_financing_agreement_data"):
|
||||||
|
return LendorFinancingAgreementSerializer(
|
||||||
|
value.lendor_financing_agreement_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"]
|
||||||
292
dta_service/core/serializers/property.py
Normal file
292
dta_service/core/serializers/property.py
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import (
|
||||||
|
Property,
|
||||||
|
PropertyPictures,
|
||||||
|
PropertySave,
|
||||||
|
SchoolInfo,
|
||||||
|
PropertyTaxInfo,
|
||||||
|
PropertySaleInfo,
|
||||||
|
PropertyWalkScoreInfo,
|
||||||
|
)
|
||||||
|
from .property_owner import PropertyOwnerSerializer
|
||||||
|
from .property_info import (
|
||||||
|
SchoolInfoSerializer,
|
||||||
|
PropertyWalkScoreInfoSerializer,
|
||||||
|
PropertyTaxInfoSerializer,
|
||||||
|
PropertySaleInfoSerializer,
|
||||||
|
OpenHouseSerializer,
|
||||||
|
)
|
||||||
|
from .document import DocumentSerializer
|
||||||
|
from core.services.document_service import DocumentService
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyPictureSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PropertyPictures
|
||||||
|
fields = ["id", "created_at", "updated_at", "image", "Property"]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
property_id = self.context["request"].data.get("Property")
|
||||||
|
try:
|
||||||
|
property_instance = Property.objects.get(id=property_id)
|
||||||
|
|
||||||
|
except Property.DoesNotExist:
|
||||||
|
raise serializers.ValidationError("Invalid property ID.")
|
||||||
|
|
||||||
|
validated_data["Property"] = property_instance
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
owner = PropertyOwnerSerializer()
|
||||||
|
documents = DocumentSerializer(many=True, read_only=True)
|
||||||
|
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",
|
||||||
|
"owner",
|
||||||
|
"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",
|
||||||
|
"documents",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at", "documents"]
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyRequestSerializer(serializers.ModelSerializer):
|
||||||
|
schools = SchoolInfoSerializer(many=True)
|
||||||
|
tax_info = PropertyTaxInfoSerializer()
|
||||||
|
sale_info = PropertySaleInfoSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Property
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"owner",
|
||||||
|
"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",
|
||||||
|
"tax_info",
|
||||||
|
"sale_info",
|
||||||
|
"schools",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at", "views", "saves"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# tax_info_data = validated_data.pop("tax_info")
|
||||||
|
# tax_info_data = validated_data.pop("tax_info")
|
||||||
|
walk_score = validated_data.pop("walk_score")
|
||||||
|
schools_data = validated_data.pop("schools")
|
||||||
|
tax_info = validated_data.pop("tax_info")
|
||||||
|
sale_info = validated_data.pop("sale_info")
|
||||||
|
schools = []
|
||||||
|
|
||||||
|
property_instance = Property.objects.create(**validated_data)
|
||||||
|
|
||||||
|
# Automatically create Lendor Financing Agreement
|
||||||
|
user = self.context["request"].user
|
||||||
|
DocumentService.create_lendor_financing_agreement(
|
||||||
|
property_instance=property_instance,
|
||||||
|
file=None,
|
||||||
|
uploaded_by=user,
|
||||||
|
description="Automatically created Lendor Financing Agreement"
|
||||||
|
)
|
||||||
|
|
||||||
|
sale_infos = []
|
||||||
|
for sale_in in sale_info:
|
||||||
|
sale_infos.append(
|
||||||
|
PropertySaleInfo.objects.create(**sale_in, property=property_instance)
|
||||||
|
)
|
||||||
|
|
||||||
|
for school_data in schools_data:
|
||||||
|
schools.append(
|
||||||
|
SchoolInfo.objects.create(**school_data, property=property_instance)
|
||||||
|
)
|
||||||
|
|
||||||
|
PropertyTaxInfo.objects.create(**tax_info, property=property_instance)
|
||||||
|
walk_score.property = property_instance
|
||||||
|
walk_score.save()
|
||||||
|
|
||||||
|
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":
|
||||||
|
# Check if Attorney Engagement Letter is accepted
|
||||||
|
if not DocumentService.check_engagement_letter_accepted(instance):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Cannot list property as active without an accepted Attorney Engagement Letter."
|
||||||
|
)
|
||||||
|
# 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 = PropertyWalkScoreInfo.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 PropertySaveSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for the PropertySave model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
property = serializers.PrimaryKeyRelatedField(queryset=Property.objects.all())
|
||||||
|
user = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PropertySave
|
||||||
|
fields = ["id", "user", "property", "created_at"]
|
||||||
|
read_only_fields = ["created_at"]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
"""
|
||||||
|
Check for a unique user-property combination before creation.
|
||||||
|
"""
|
||||||
|
user = self.context["request"].user
|
||||||
|
property_id = data.get("property").id
|
||||||
|
if PropertySave.objects.filter(user=user, property=property_id).exists():
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"This property is already saved by the user."
|
||||||
|
)
|
||||||
|
return data
|
||||||
111
dta_service/core/serializers/property_info.py
Normal file
111
dta_service/core/serializers/property_info.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import (
|
||||||
|
SchoolInfo,
|
||||||
|
PropertyWalkScoreInfo,
|
||||||
|
PropertyTaxInfo,
|
||||||
|
PropertySaleInfo,
|
||||||
|
OpenHouse,
|
||||||
|
Property,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SchoolInfoSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SchoolInfo
|
||||||
|
fields = [
|
||||||
|
"city",
|
||||||
|
"state",
|
||||||
|
"zip_code",
|
||||||
|
"latitude",
|
||||||
|
"longitude",
|
||||||
|
"enrollment",
|
||||||
|
"grades",
|
||||||
|
"name",
|
||||||
|
"parent_rating",
|
||||||
|
"rating",
|
||||||
|
"school_type",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyWalkScoreInfoSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PropertyWalkScoreInfo
|
||||||
|
fields = [
|
||||||
|
"walk_score",
|
||||||
|
"walk_description",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"ws_link",
|
||||||
|
"logo_url",
|
||||||
|
"transit_score",
|
||||||
|
"transit_description",
|
||||||
|
"transit_summary",
|
||||||
|
"bike_score",
|
||||||
|
"bike_description",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
property_id = self.context["request"].data.get("property")
|
||||||
|
try:
|
||||||
|
property_instance = Property.objects.get(id=property_id)
|
||||||
|
|
||||||
|
except Property.DoesNotExist:
|
||||||
|
raise serializers.ValidationError("Invalid property ID.")
|
||||||
|
|
||||||
|
validated_data["property"] = property_instance
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyTaxInfoSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PropertyTaxInfo
|
||||||
|
fields = [
|
||||||
|
"assessed_value",
|
||||||
|
"assessment_year",
|
||||||
|
"tax_amount",
|
||||||
|
"year",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class PropertySaleInfoSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PropertySaleInfo
|
||||||
|
fields = [
|
||||||
|
"seq_no",
|
||||||
|
"sale_date",
|
||||||
|
"sale_amount",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class OpenHouseSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = OpenHouse
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"listed_date",
|
||||||
|
"property",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
property_id = self.context["request"].data.get("property")
|
||||||
|
try:
|
||||||
|
property_instance = Property.objects.get(id=property_id)
|
||||||
|
|
||||||
|
except Property.DoesNotExist:
|
||||||
|
raise serializers.ValidationError("Invalid property ID.")
|
||||||
|
|
||||||
|
validated_data["property"] = property_instance
|
||||||
|
return super().create(validated_data)
|
||||||
31
dta_service/core/serializers/property_owner.py
Normal file
31
dta_service/core/serializers/property_owner.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import PropertyOwner, User
|
||||||
|
from .user import UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyOwnerSerializer(serializers.ModelSerializer):
|
||||||
|
user = UserSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PropertyOwner
|
||||||
|
fields = ["user", "phone_number"]
|
||||||
|
read_only_fields = ["created_at", "updated_at"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
user_data = validated_data.pop("user")
|
||||||
|
user = User.objects.create_user(**user_data)
|
||||||
|
property_owner = PropertyOwner.objects.create(user=user, **validated_data)
|
||||||
|
return property_owner
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
user_data = validated_data.pop("user", None)
|
||||||
|
if user_data:
|
||||||
|
user = instance.user
|
||||||
|
for attr, value in user_data.items():
|
||||||
|
setattr(user, attr, value)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
for attr, value in validated_data.items():
|
||||||
|
setattr(instance, attr, value)
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
82
dta_service/core/serializers/real_estate_agent.py
Normal file
82
dta_service/core/serializers/real_estate_agent.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import RealEstateAgent
|
||||||
|
from .user import UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class RealEstateAgentSerializer(serializers.ModelSerializer):
|
||||||
|
user = UserSerializer(
|
||||||
|
read_only=True
|
||||||
|
) # Nested serializer for the related User object
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RealEstateAgent
|
||||||
|
fields = [
|
||||||
|
"user",
|
||||||
|
"brokerage_name",
|
||||||
|
"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",
|
||||||
|
"latitude",
|
||||||
|
]
|
||||||
|
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 RealEstateAgent."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the user_type is correctly set for the new user
|
||||||
|
if user_instance.user_type != "real_estate_agent":
|
||||||
|
user_instance.user_type = "real_estate_agent"
|
||||||
|
user_instance.save()
|
||||||
|
|
||||||
|
agent = RealEstateAgent.objects.create(user=user_instance, **validated_data)
|
||||||
|
return agent
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# Handle updates for RealEstateAgent fields
|
||||||
|
instance.brokerage_name = validated_data.get(
|
||||||
|
"brokerage_name", instance.brokerage_name
|
||||||
|
)
|
||||||
|
instance.license_number = validated_data.get(
|
||||||
|
"license_number", instance.license_number
|
||||||
|
)
|
||||||
|
instance.phone_number = validated_data.get(
|
||||||
|
"phone_number", instance.phone_number
|
||||||
|
)
|
||||||
|
instance.address = validated_data.get("address", instance.address)
|
||||||
|
instance.city = validated_data.get("city", instance.city)
|
||||||
|
instance.state = validated_data.get("state", instance.state)
|
||||||
|
instance.zip_code = validated_data.get("zip_code", instance.zip_code)
|
||||||
|
instance.specialties = validated_data.get("specialties", instance.specialties)
|
||||||
|
instance.years_experience = validated_data.get(
|
||||||
|
"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()
|
||||||
|
return instance
|
||||||
59
dta_service/core/serializers/support.py
Normal file
59
dta_service/core/serializers/support.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import SupportMessage, SupportCase, FAQ
|
||||||
|
|
||||||
|
|
||||||
|
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"]
|
||||||
29
dta_service/core/serializers/support_agent.py
Normal file
29
dta_service/core/serializers/support_agent.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import SupportAgent
|
||||||
|
from .user import UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
146
dta_service/core/serializers/user.py
Normal file
146
dta_service/core/serializers/user.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from core.models import User, PasswordResetToken
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||||
|
@classmethod
|
||||||
|
def get_token(cls, user):
|
||||||
|
token = super().get_token(user)
|
||||||
|
|
||||||
|
# Add custom claims
|
||||||
|
token["email"] = user.email
|
||||||
|
token["first_name"] = user.first_name
|
||||||
|
token["last_name"] = user.last_name
|
||||||
|
token["user_type"] = user.user_type
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
data = super().validate(attrs)
|
||||||
|
|
||||||
|
# Add additional responses
|
||||||
|
refresh = self.get_token(self.user)
|
||||||
|
data["refresh"] = str(refresh)
|
||||||
|
data["access"] = str(refresh.access_token)
|
||||||
|
|
||||||
|
# Add user details
|
||||||
|
data["user"] = {
|
||||||
|
"email": self.user.email,
|
||||||
|
"first_name": self.user.first_name,
|
||||||
|
"last_name": self.user.last_name,
|
||||||
|
"user_type": self.user.user_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"user_type",
|
||||||
|
"is_active",
|
||||||
|
"date_joined",
|
||||||
|
"tos_signed",
|
||||||
|
"profile_created",
|
||||||
|
"tier",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "is_active", "date_joined"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegisterSerializer(serializers.ModelSerializer):
|
||||||
|
password = serializers.CharField(write_only=True, required=True)
|
||||||
|
password2 = serializers.CharField(write_only=True, required=True)
|
||||||
|
vendor_type = serializers.CharField(write_only=True, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"user_type",
|
||||||
|
"vendor_type",
|
||||||
|
"password",
|
||||||
|
"password2",
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"password": {"write_only": True},
|
||||||
|
"password2": {"write_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
if attrs["password"] != attrs["password2"]:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"password": "Password fields didn't match."}
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Remove password confirmation and optional vendor_type before creating the user
|
||||||
|
validated_data.pop("password2", None)
|
||||||
|
validated_data.pop("vendor_type", None)
|
||||||
|
user = User.objects.create_user(**validated_data)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetRequestSerializer(serializers.Serializer):
|
||||||
|
email = serializers.EmailField()
|
||||||
|
|
||||||
|
def validate_email(self, value):
|
||||||
|
try:
|
||||||
|
User.objects.get(email=value)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise serializers.ValidationError("No user with this email address exists.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
user = User.objects.get(email=self.validated_data["email"])
|
||||||
|
expires_at = datetime.now() + timedelta(hours=24)
|
||||||
|
token = PasswordResetToken.objects.create(user=user, expires_at=expires_at)
|
||||||
|
token.send_reset_email()
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetConfirmSerializer(serializers.Serializer):
|
||||||
|
token = serializers.UUIDField()
|
||||||
|
new_password = serializers.CharField(write_only=True)
|
||||||
|
new_password2 = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
try:
|
||||||
|
token = PasswordResetToken.objects.get(token=attrs["token"])
|
||||||
|
except PasswordResetToken.DoesNotExist:
|
||||||
|
raise serializers.ValidationError({"token": "Invalid token."})
|
||||||
|
|
||||||
|
if not token.is_valid():
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"token": "Token is invalid or has expired."}
|
||||||
|
)
|
||||||
|
|
||||||
|
if attrs["new_password"] != attrs["new_password2"]:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"new_password": "Password fields didn't match."}
|
||||||
|
)
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
token = PasswordResetToken.objects.get(token=self.validated_data["token"])
|
||||||
|
user = token.user
|
||||||
|
user.set_password(self.validated_data["new_password"])
|
||||||
|
user.save()
|
||||||
|
token.used = True
|
||||||
|
token.save()
|
||||||
|
return user
|
||||||
64
dta_service/core/serializers/vendor.py
Normal file
64
dta_service/core/serializers/vendor.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import Vendor, User
|
||||||
|
from .user import UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class VendorSerializer(serializers.ModelSerializer):
|
||||||
|
user = UserSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Vendor
|
||||||
|
fields = [
|
||||||
|
# List all Vendor fields you want to expose/update, but not the user field
|
||||||
|
"business_name",
|
||||||
|
"business_type",
|
||||||
|
"phone_number",
|
||||||
|
"address",
|
||||||
|
"city",
|
||||||
|
"state",
|
||||||
|
"zip_code",
|
||||||
|
"description",
|
||||||
|
"website",
|
||||||
|
"services",
|
||||||
|
"service_areas",
|
||||||
|
"certifications",
|
||||||
|
"longitude",
|
||||||
|
"latitude",
|
||||||
|
"profile_picture",
|
||||||
|
"user",
|
||||||
|
"views",
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"average_rating",
|
||||||
|
"num_reviews",
|
||||||
|
]
|
||||||
|
|
||||||
|
# This create method is fine for creating a new vendor and user
|
||||||
|
def create(self, validated_data):
|
||||||
|
user_data = validated_data.pop("user")
|
||||||
|
user = User.objects.create_user(
|
||||||
|
**user_data
|
||||||
|
) # Use create_user to hash the password if present
|
||||||
|
vendor = Vendor.objects.create(user=user, **validated_data)
|
||||||
|
return vendor
|
||||||
|
|
||||||
|
# Override the update method to handle the nested user data
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# Pop the user data to handle it separately
|
||||||
|
user_data = validated_data.pop("user", {})
|
||||||
|
user_instance = instance.user
|
||||||
|
|
||||||
|
# Update the Vendor instance fields
|
||||||
|
for attr, value in validated_data.items():
|
||||||
|
setattr(instance, attr, value)
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
# Update the nested User instance fields
|
||||||
|
for attr, value in user_data.items():
|
||||||
|
setattr(user_instance, attr, value)
|
||||||
|
user_instance.save()
|
||||||
|
|
||||||
|
return instance
|
||||||
92
dta_service/core/serializers/video.py
Normal file
92
dta_service/core/serializers/video.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from core.models import VideoCategory, Video, UserVideoProgress
|
||||||
|
|
||||||
|
|
||||||
|
class VideoCategorySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = VideoCategory
|
||||||
|
fields = ["id", "name", "description"]
|
||||||
|
read_only_fields = ["id"]
|
||||||
|
|
||||||
|
|
||||||
|
class VideoSerializer(serializers.ModelSerializer):
|
||||||
|
category = VideoCategorySerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Video
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"category",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"link",
|
||||||
|
"duration",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Extract category data
|
||||||
|
category_data = validated_data.pop("category")
|
||||||
|
|
||||||
|
# Get or create category
|
||||||
|
category, _ = VideoCategory.objects.get_or_create(**category_data)
|
||||||
|
|
||||||
|
# Create video with the category
|
||||||
|
video = Video.objects.create(category=category, **validated_data)
|
||||||
|
return video
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# Handle category update if provided
|
||||||
|
category_data = validated_data.pop("category", None)
|
||||||
|
if category_data:
|
||||||
|
category, _ = VideoCategory.objects.get_or_create(**category_data)
|
||||||
|
instance.category = category
|
||||||
|
|
||||||
|
# Update other fields
|
||||||
|
for attr, value in validated_data.items():
|
||||||
|
setattr(instance, attr, value)
|
||||||
|
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class UserVideoProgressSerializer(serializers.ModelSerializer):
|
||||||
|
video = VideoSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserVideoProgress
|
||||||
|
fields = ["id", "video", "progress", "status", "last_watched", "user"]
|
||||||
|
read_only_fields = ["id", "status", "last_watched"]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Extract video data
|
||||||
|
video_data = validated_data.pop("video")
|
||||||
|
|
||||||
|
# Get or create video
|
||||||
|
video_serializer = VideoSerializer(data=video_data)
|
||||||
|
video_serializer.is_valid(raise_exception=True)
|
||||||
|
video = video_serializer.save()
|
||||||
|
user = validated_data.pop("user")
|
||||||
|
# Create progress record
|
||||||
|
progress = UserVideoProgress.objects.create(
|
||||||
|
user=user, video=video, **validated_data
|
||||||
|
)
|
||||||
|
return progress
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# Handle video update if provided
|
||||||
|
video_data = validated_data.pop("video", None)
|
||||||
|
if video_data:
|
||||||
|
video_serializer = VideoSerializer(
|
||||||
|
instance.video, data=video_data, partial=True
|
||||||
|
)
|
||||||
|
video_serializer.is_valid(raise_exception=True)
|
||||||
|
video_serializer.save()
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
instance.progress = validated_data.get("progress", instance.progress)
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
return instance
|
||||||
108
dta_service/core/services/document_service.py
Normal file
108
dta_service/core/services/document_service.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
from core.models import Document, AttorneyEngagementLetter, Attorney, User, LendorFinancingAgreement
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class DocumentService:
|
||||||
|
@staticmethod
|
||||||
|
def create_document(property_instance, document_type, file, uploaded_by, description=None, shared_with=None):
|
||||||
|
"""
|
||||||
|
Creates a generic Document instance.
|
||||||
|
"""
|
||||||
|
document = Document.objects.create(
|
||||||
|
property=property_instance,
|
||||||
|
document_type=document_type,
|
||||||
|
file=file,
|
||||||
|
uploaded_by=uploaded_by,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
|
||||||
|
if shared_with:
|
||||||
|
document.shared_with.set(shared_with)
|
||||||
|
|
||||||
|
return document
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_attorney_engagement_letter(property_instance, file, uploaded_by, attorney_id, description=None):
|
||||||
|
"""
|
||||||
|
Creates an Attorney Engagement Letter document and its specific data.
|
||||||
|
"""
|
||||||
|
document = DocumentService.create_document(
|
||||||
|
property_instance=property_instance,
|
||||||
|
document_type="attorney_engagement_letter",
|
||||||
|
file=file,
|
||||||
|
uploaded_by=uploaded_by,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
attorney = Attorney.objects.get(user__id=attorney_id)
|
||||||
|
except Attorney.DoesNotExist:
|
||||||
|
# Handle case where attorney doesn't exist, maybe raise an error or set to None
|
||||||
|
# For now, we'll assume valid ID or let it fail if critical
|
||||||
|
raise ValueError(f"Attorney with ID {attorney_id} not found.")
|
||||||
|
|
||||||
|
AttorneyEngagementLetter.objects.create(
|
||||||
|
document=document,
|
||||||
|
attorney=attorney
|
||||||
|
)
|
||||||
|
|
||||||
|
return document
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_lendor_financing_agreement(property_instance, file, uploaded_by, description=None):
|
||||||
|
"""
|
||||||
|
Creates a Lendor Financing Agreement document.
|
||||||
|
"""
|
||||||
|
document = DocumentService.create_document(
|
||||||
|
property_instance=property_instance,
|
||||||
|
document_type="lendor_financing_agreement",
|
||||||
|
file=file,
|
||||||
|
uploaded_by=uploaded_by,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
|
||||||
|
LendorFinancingAgreement.objects.create(
|
||||||
|
document=document
|
||||||
|
)
|
||||||
|
|
||||||
|
return document
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_engagement_letter_accepted(property_instance):
|
||||||
|
"""
|
||||||
|
Checks if there is an accepted Attorney Engagement Letter for the property.
|
||||||
|
If no letter exists, it creates one linked to the default attorney.
|
||||||
|
"""
|
||||||
|
# Find all engagement letters for this property
|
||||||
|
engagement_letters = AttorneyEngagementLetter.objects.filter(
|
||||||
|
document__property=property_instance,
|
||||||
|
document__document_type="attorney_engagement_letter"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not engagement_letters.exists():
|
||||||
|
# Create one with default attorney
|
||||||
|
try:
|
||||||
|
default_attorney_user = User.objects.get(email="ryan@relawfirm")
|
||||||
|
# We need to pass a user for 'uploaded_by'.
|
||||||
|
# Ideally this should be the property owner, but we only have property_instance.
|
||||||
|
# We can try to get the owner from property_instance.owner.user
|
||||||
|
uploaded_by = property_instance.owner.user if property_instance.owner else None
|
||||||
|
|
||||||
|
DocumentService.create_attorney_engagement_letter(
|
||||||
|
property_instance=property_instance,
|
||||||
|
file=None, # No file initially
|
||||||
|
uploaded_by=uploaded_by,
|
||||||
|
attorney_id=default_attorney_user.id,
|
||||||
|
description="Automatically created Engagement Letter"
|
||||||
|
)
|
||||||
|
return False # Created but not accepted
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# If default attorney doesn't exist (e.g. migration didn't run or test env),
|
||||||
|
# we can't create it. Just return False.
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if any of them are accepted
|
||||||
|
for letter in engagement_letters:
|
||||||
|
if letter.is_accepted:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
65
dta_service/core/tests/test_document_service.py
Normal file
65
dta_service/core/tests/test_document_service.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from core.models import Property, PropertyOwner, User, Attorney, Document, AttorneyEngagementLetter
|
||||||
|
from core.services.document_service import DocumentService
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class DocumentServiceTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(email="test@example.com", first_name="Test", last_name="User")
|
||||||
|
self.owner_user = User.objects.create(email="owner@example.com", first_name="Owner", last_name="User", user_type="property_owner")
|
||||||
|
self.owner = PropertyOwner.objects.create(user=self.owner_user)
|
||||||
|
self.attorney_user = User.objects.create(email="attorney@example.com", first_name="Attorney", last_name="User", user_type="attorney")
|
||||||
|
self.attorney = Attorney.objects.create(user=self.attorney_user, firm_name="Test Firm", address="123 Law St", city="Lawville", state="CA", zip_code="90210")
|
||||||
|
|
||||||
|
# Create default attorney for auto-creation test
|
||||||
|
self.default_attorney_user = User.objects.create(email="ryan@relawfirm", first_name="Ryan", last_name="Attorney", user_type="attorney")
|
||||||
|
self.default_attorney = Attorney.objects.create(user=self.default_attorney_user, firm_name="The Real Estate Law Firm, LLC", address="505 W Main St Suite A", city="St. Charles", state="IL", zip_code="60174")
|
||||||
|
|
||||||
|
self.property = Property.objects.create(
|
||||||
|
owner=self.owner,
|
||||||
|
address="123 Test St",
|
||||||
|
city="Test City",
|
||||||
|
state="CA",
|
||||||
|
zip_code="12345",
|
||||||
|
market_value=100000,
|
||||||
|
realestate_api_id=123
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_attorney_engagement_letter(self):
|
||||||
|
document = DocumentService.create_attorney_engagement_letter(
|
||||||
|
property_instance=self.property,
|
||||||
|
file=None,
|
||||||
|
uploaded_by=self.owner_user,
|
||||||
|
attorney_id=self.attorney_user.id,
|
||||||
|
description="Engagement Letter"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(document.document_type, "attorney_engagement_letter")
|
||||||
|
self.assertTrue(hasattr(document, "attorney_engagement_letter_data"))
|
||||||
|
self.assertEqual(document.attorney_engagement_letter_data.attorney, self.attorney)
|
||||||
|
self.assertFalse(document.attorney_engagement_letter_data.is_accepted)
|
||||||
|
|
||||||
|
def test_check_engagement_letter_accepted(self):
|
||||||
|
# No letter yet -> Should auto-create one linked to default attorney
|
||||||
|
self.assertFalse(DocumentService.check_engagement_letter_accepted(self.property))
|
||||||
|
|
||||||
|
# Verify it was created
|
||||||
|
engagement_letters = AttorneyEngagementLetter.objects.filter(
|
||||||
|
document__property=self.property,
|
||||||
|
document__document_type="attorney_engagement_letter"
|
||||||
|
)
|
||||||
|
self.assertTrue(engagement_letters.exists())
|
||||||
|
self.assertEqual(engagement_letters.count(), 1)
|
||||||
|
self.assertEqual(engagement_letters.first().attorney, self.default_attorney)
|
||||||
|
|
||||||
|
# Still not accepted
|
||||||
|
self.assertFalse(DocumentService.check_engagement_letter_accepted(self.property))
|
||||||
|
|
||||||
|
# Accept letter
|
||||||
|
letter = engagement_letters.first()
|
||||||
|
letter.is_accepted = True
|
||||||
|
letter.accepted_at = timezone.now()
|
||||||
|
letter.save()
|
||||||
|
|
||||||
|
# Should be accepted now
|
||||||
|
self.assertTrue(DocumentService.check_engagement_letter_accepted(self.property))
|
||||||
71
dta_service/core/tests/test_document_signing.py
Normal file
71
dta_service/core/tests/test_document_signing.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework import status
|
||||||
|
from core.models import Property, PropertyOwner, User, Attorney, Document, AttorneyEngagementLetter
|
||||||
|
from core.services.document_service import DocumentService
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class DocumentSigningTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create(email="test@example.com", first_name="Test", last_name="User", user_type="property_owner")
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
self.owner = PropertyOwner.objects.create(user=self.user)
|
||||||
|
self.attorney_user = User.objects.create(email="attorney@example.com", first_name="Attorney", last_name="User", user_type="attorney")
|
||||||
|
self.attorney = Attorney.objects.create(user=self.attorney_user, firm_name="Test Firm", address="123 Law St", city="Lawville", state="CA", zip_code="90210")
|
||||||
|
|
||||||
|
self.property = Property.objects.create(
|
||||||
|
owner=self.owner,
|
||||||
|
address="123 Test St",
|
||||||
|
city="Test City",
|
||||||
|
state="CA",
|
||||||
|
zip_code="12345",
|
||||||
|
market_value=100000,
|
||||||
|
realestate_api_id=123
|
||||||
|
)
|
||||||
|
|
||||||
|
self.document = DocumentService.create_attorney_engagement_letter(
|
||||||
|
property_instance=self.property,
|
||||||
|
file=None,
|
||||||
|
uploaded_by=self.user,
|
||||||
|
attorney_id=self.attorney_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sign_engagement_letter(self):
|
||||||
|
url = f"/api/documents/{self.document.id}/sign/"
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data["detail"], "Attorney Engagement Letter accepted successfully.")
|
||||||
|
|
||||||
|
self.document.refresh_from_db()
|
||||||
|
self.assertTrue(self.document.attorney_engagement_letter_data.is_accepted)
|
||||||
|
self.assertIsNotNone(self.document.attorney_engagement_letter_data.accepted_at)
|
||||||
|
|
||||||
|
def test_sign_already_accepted_letter(self):
|
||||||
|
# Accept it first
|
||||||
|
letter = self.document.attorney_engagement_letter_data
|
||||||
|
letter.is_accepted = True
|
||||||
|
letter.accepted_at = timezone.now()
|
||||||
|
letter.save()
|
||||||
|
|
||||||
|
url = f"/api/documents/{self.document.id}/sign/"
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.data["detail"], "This letter has already been accepted.")
|
||||||
|
|
||||||
|
def test_sign_wrong_document_type(self):
|
||||||
|
# Create a generic document
|
||||||
|
other_doc = Document.objects.create(
|
||||||
|
property=self.property,
|
||||||
|
document_type="other",
|
||||||
|
uploaded_by=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
url = f"/api/documents/{other_doc.id}/sign/"
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.data["detail"], "This document is not an Attorney Engagement Letter.")
|
||||||
53
dta_service/core/tests/test_property_serializer.py
Normal file
53
dta_service/core/tests/test_property_serializer.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from core.models import Property, PropertyOwner, User, Attorney
|
||||||
|
from core.serializers.property import PropertyRequestSerializer
|
||||||
|
from core.services.document_service import DocumentService
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class PropertySerializerValidationTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.owner_user = User.objects.create(email="owner@example.com", first_name="Owner", last_name="User", user_type="property_owner")
|
||||||
|
self.owner = PropertyOwner.objects.create(user=self.owner_user)
|
||||||
|
self.attorney_user = User.objects.create(email="attorney@example.com", first_name="Attorney", last_name="User", user_type="attorney")
|
||||||
|
self.attorney = Attorney.objects.create(user=self.attorney_user, firm_name="Test Firm", address="123 Law St", city="Lawville", state="CA", zip_code="90210")
|
||||||
|
|
||||||
|
self.property = Property.objects.create(
|
||||||
|
owner=self.owner,
|
||||||
|
address="123 Test St",
|
||||||
|
city="Test City",
|
||||||
|
state="CA",
|
||||||
|
zip_code="12345",
|
||||||
|
market_value=100000,
|
||||||
|
realestate_api_id=123,
|
||||||
|
property_status="off_market"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_status_active_without_letter_fails(self):
|
||||||
|
serializer = PropertyRequestSerializer(instance=self.property, data={"property_status": "active"}, partial=True)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError) as cm:
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
self.assertIn("Cannot list property as active without an accepted Attorney Engagement Letter.", str(cm.exception))
|
||||||
|
|
||||||
|
def test_update_status_active_with_accepted_letter_succeeds(self):
|
||||||
|
# Create and accept letter
|
||||||
|
document = DocumentService.create_attorney_engagement_letter(
|
||||||
|
property_instance=self.property,
|
||||||
|
file=None,
|
||||||
|
uploaded_by=self.owner_user,
|
||||||
|
attorney_id=self.attorney_user.id
|
||||||
|
)
|
||||||
|
letter = document.attorney_engagement_letter_data
|
||||||
|
letter.is_accepted = True
|
||||||
|
letter.accepted_at = timezone.now()
|
||||||
|
letter.save()
|
||||||
|
|
||||||
|
serializer = PropertyRequestSerializer(instance=self.property, data={"property_status": "active"}, partial=True)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
updated_property = serializer.save()
|
||||||
|
|
||||||
|
self.assertEqual(updated_property.property_status, "active")
|
||||||
|
self.assertIsNotNone(updated_property.listed_date)
|
||||||
@@ -3,9 +3,9 @@ from rest_framework.routers import DefaultRouter
|
|||||||
from core.views import SupportCaseViewSet, SupportMessageViewSet, FAQViewSet
|
from core.views import SupportCaseViewSet, SupportMessageViewSet, FAQViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r"cases", SupportCaseViewSet)
|
router.register(r"cases", SupportCaseViewSet, basename="support-case")
|
||||||
router.register(r"messages", SupportMessageViewSet)
|
router.register(r"messages", SupportMessageViewSet, basename="support-message")
|
||||||
router.register(r"faq", FAQViewSet)
|
router.register(r"faq", FAQViewSet, basename="faq")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include(router.urls)),
|
path("", include(router.urls)),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
47
dta_service/core/views/__init__.py
Normal file
47
dta_service/core/views/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from .user import (
|
||||||
|
CustomTokenObtainPairView,
|
||||||
|
UserRegisterView,
|
||||||
|
UserRetrieveView,
|
||||||
|
UserSignTosView,
|
||||||
|
LogoutView,
|
||||||
|
PasswordResetRequestView,
|
||||||
|
PasswordResetConfirmView,
|
||||||
|
)
|
||||||
|
from .property_owner import PropertyOwnerViewSet
|
||||||
|
from .vendor import VendorViewSet
|
||||||
|
from .attorney import AttorneyViewSet
|
||||||
|
from .real_estate_agent import RealEstateAgentViewSet
|
||||||
|
from .support_agent import SupportAgentViewSet
|
||||||
|
from .property import (
|
||||||
|
PropertyViewSet,
|
||||||
|
PropertyPictureViewSet,
|
||||||
|
PropertyDescriptionView,
|
||||||
|
PropertySaveViewSet,
|
||||||
|
PropertDetailProxyView,
|
||||||
|
AutoCompleteProxyView,
|
||||||
|
)
|
||||||
|
from .property_info import OpenHouseViewSet
|
||||||
|
from .video import (
|
||||||
|
VideoCategoryViewSet,
|
||||||
|
VideoViewSet,
|
||||||
|
UserVideoProgressViewSet,
|
||||||
|
)
|
||||||
|
from .conversation import (
|
||||||
|
ConversationViewSet,
|
||||||
|
MessageViewSet,
|
||||||
|
)
|
||||||
|
from .bid import (
|
||||||
|
BidViewSet,
|
||||||
|
BidResponseViewSet,
|
||||||
|
)
|
||||||
|
from .document import (
|
||||||
|
DocumentViewSet,
|
||||||
|
RetrieveDocumentView,
|
||||||
|
CreateDocumentView,
|
||||||
|
)
|
||||||
|
from .support import (
|
||||||
|
SupportCaseViewSet,
|
||||||
|
FAQListView,
|
||||||
|
SupportMessageViewSet,
|
||||||
|
FAQViewSet,
|
||||||
|
)
|
||||||
26
dta_service/core/views/attorney.py
Normal file
26
dta_service/core/views/attorney.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from core.models import Attorney
|
||||||
|
from core.serializers import AttorneySerializer
|
||||||
|
|
||||||
|
|
||||||
|
class AttorneyViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = AttorneySerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
# When creating an Attorney, link it to the currently authenticated user
|
||||||
|
# or handle user creation/association logic here.
|
||||||
|
# For demonstration, we'll assume request.user is the user to link.
|
||||||
|
# In a real app, you might have more complex logic (e.g., admin creating for another user).
|
||||||
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if user.user_type == "attorney":
|
||||||
|
if not Attorney.objects.filter(user=user).exists():
|
||||||
|
return Attorney.objects.create(user=user)
|
||||||
|
|
||||||
|
return Attorney.objects.filter(user=user)
|
||||||
|
else:
|
||||||
|
return Attorney.objects.all()
|
||||||
70
dta_service/core/views/bid.py
Normal file
70
dta_service/core/views/bid.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from rest_framework import viewsets, serializers
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from core.models import Bid, BidResponse
|
||||||
|
from core.serializers import BidSerializer, BidResponseSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BidViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if user.user_type == "property_owner":
|
||||||
|
return Bid.objects.filter(property__owner__user=user).order_by(
|
||||||
|
"-created_at"
|
||||||
|
)
|
||||||
|
elif user.user_type == "vendor":
|
||||||
|
# Vendors should see all bids, but only their own responses
|
||||||
|
return Bid.objects.all().order_by("-created_at")
|
||||||
|
return Bid.objects.none()
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
return BidSerializer
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def select_response(self, request, pk=None):
|
||||||
|
bid = self.get_object()
|
||||||
|
response_id = request.data.get("response_id")
|
||||||
|
try:
|
||||||
|
response = BidResponse.objects.get(id=response_id, bid=bid)
|
||||||
|
# Ensure the current user is the property owner of the bid
|
||||||
|
if request.user == bid.property.owner.user:
|
||||||
|
# Unselect any previously selected response for this bid
|
||||||
|
BidResponse.objects.filter(bid=bid, status="selected").update(
|
||||||
|
status="submitted"
|
||||||
|
)
|
||||||
|
# Select the new response
|
||||||
|
response.status = "selected"
|
||||||
|
response.save()
|
||||||
|
return Response({"status": "response selected"})
|
||||||
|
return Response(
|
||||||
|
{"error": "You do not have permission to perform this action."},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
except BidResponse.DoesNotExist:
|
||||||
|
return Response({"error": "Response not found."}, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
class BidResponseViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = BidResponseSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if user.user_type == "property_owner":
|
||||||
|
return BidResponse.objects.filter(bid__property__owner__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()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
# A vendor can only create one response per bid
|
||||||
|
bid = serializer.validated_data["bid"]
|
||||||
|
vendor = self.request.user.vendor
|
||||||
|
if BidResponse.objects.filter(bid=bid, vendor=vendor).exists():
|
||||||
|
raise serializers.ValidationError("You have already responded to this bid.")
|
||||||
|
serializer.save(vendor=vendor, status="submitted")
|
||||||
71
dta_service/core/views/conversation.py
Normal file
71
dta_service/core/views/conversation.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from core.models import Conversation, Message, PropertyOwner, Vendor
|
||||||
|
from core.serializers import (
|
||||||
|
ConversationResponseSerializer,
|
||||||
|
ConversationRequestSerializer,
|
||||||
|
MessageSerializer,
|
||||||
|
)
|
||||||
|
from core.permissions import IsParticipant
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated, IsParticipant]
|
||||||
|
search_fields = ["vendor", "property_owner"]
|
||||||
|
|
||||||
|
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 ConversationResponseSerializer
|
||||||
|
return ConversationRequestSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if user.user_type == "property_owner":
|
||||||
|
owner = PropertyOwner.objects.get(user=user)
|
||||||
|
vendor_id = self.request.query_params.get("vendor")
|
||||||
|
if vendor_id:
|
||||||
|
return Conversation.objects.filter(
|
||||||
|
property_owner=owner, vendor=vendor_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Conversation.objects.filter(property_owner=owner)
|
||||||
|
elif user.user_type == "vendor":
|
||||||
|
vendor = Vendor.objects.get(user=user)
|
||||||
|
return Conversation.objects.filter(vendor=vendor)
|
||||||
|
return Conversation.objects.none()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
if self.request.user.user_type == "property_owner":
|
||||||
|
owner = PropertyOwner.objects.get(user=self.request.user)
|
||||||
|
serializer.save(property_owner=owner)
|
||||||
|
elif self.request.user.user_type == "vendor":
|
||||||
|
vendor = Vendor.objects.get(user=self.request.user)
|
||||||
|
serializer.save(vendor=vendor)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = MessageSerializer
|
||||||
|
permission_classes = [IsAuthenticated, IsParticipant]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
conversation_id = self.kwargs.get("conversation_id")
|
||||||
|
conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||||
|
self.check_object_permissions(self.request, conversation)
|
||||||
|
return Message.objects.filter(conversation=conversation).order_by("timestamp")
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
conversation_id = self.kwargs.get("conversation_id")
|
||||||
|
conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||||
|
self.check_object_permissions(self.request, conversation)
|
||||||
|
serializer.save(conversation=conversation, sender=self.request.user)
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
context = super().get_serializer_context()
|
||||||
|
context["conversation_id"] = self.kwargs.get("conversation_id")
|
||||||
|
return context
|
||||||
374
dta_service/core/views/document.py
Normal file
374
dta_service/core/views/document.py
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
from rest_framework import viewsets, generics, status
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.exceptions import NotFound
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db import transaction
|
||||||
|
from decimal import Decimal
|
||||||
|
from core.models import (
|
||||||
|
Document,
|
||||||
|
OfferDocument,
|
||||||
|
Property,
|
||||||
|
User,
|
||||||
|
AttorneyEngagementLetter,
|
||||||
|
LendorFinancingAgreement,
|
||||||
|
)
|
||||||
|
from core.serializers import (
|
||||||
|
DocumentSerializer,
|
||||||
|
OfferSerializer,
|
||||||
|
SellerDisclosureSerializer,
|
||||||
|
HomeImprovementReceiptSerializer,
|
||||||
|
LendorFinancingAgreementSerializer,
|
||||||
|
)
|
||||||
|
from core.permissions import IsPropertyOwnerOrVendorOrAttorney
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def sign(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
Allows a user to sign (acknowledge) an Attorney Engagement Letter.
|
||||||
|
"""
|
||||||
|
document = self.get_object()
|
||||||
|
|
||||||
|
if document.document_type != "attorney_engagement_letter":
|
||||||
|
return Response(
|
||||||
|
{"detail": "This document is not an Attorney Engagement Letter."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
engagement_letter = document.attorney_engagement_letter_data
|
||||||
|
if engagement_letter.is_accepted:
|
||||||
|
return Response(
|
||||||
|
{"detail": "This letter has already been accepted."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
engagement_letter.is_accepted = True
|
||||||
|
engagement_letter.accepted_at = timezone.now()
|
||||||
|
engagement_letter.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"detail": "Attorney Engagement Letter accepted successfully."},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
except AttorneyEngagementLetter.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Attorney Engagement Letter data not found."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
"lendor_financing_agreement": LendorFinancingAgreementSerializer,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 == "lendor_financing_agreement":
|
||||||
|
mutable_data["description"] = f"Financing Agreement for {property.address}"
|
||||||
|
|
||||||
|
# Add property owner
|
||||||
|
existing_shared_with = mutable_data.get("shared_with", [])
|
||||||
|
if not isinstance(existing_shared_with, list):
|
||||||
|
existing_shared_with = [existing_shared_with]
|
||||||
|
|
||||||
|
if property.owner and property.owner.user:
|
||||||
|
existing_shared_with.append(property.owner.user.id)
|
||||||
|
|
||||||
|
# Add attorney if exists
|
||||||
|
try:
|
||||||
|
engagement_letter = AttorneyEngagementLetter.objects.filter(
|
||||||
|
document__property=property,
|
||||||
|
is_accepted=True
|
||||||
|
).select_related('attorney__user').first()
|
||||||
|
|
||||||
|
if engagement_letter and engagement_letter.attorney:
|
||||||
|
existing_shared_with.append(engagement_letter.attorney.user.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mutable_data["shared_with"] = existing_shared_with
|
||||||
|
|
||||||
|
elif document_type == "seller_disclosure":
|
||||||
|
mutable_data["description"] = property.address
|
||||||
|
# Link default attorney
|
||||||
|
try:
|
||||||
|
default_attorney_user = User.objects.get(email="ryan@relawfirm")
|
||||||
|
# Add to shared_with. shared_with expects a list of IDs.
|
||||||
|
# If shared_with is already present, append.
|
||||||
|
existing_shared_with = mutable_data.get("shared_with", [])
|
||||||
|
if isinstance(existing_shared_with, list):
|
||||||
|
existing_shared_with.append(default_attorney_user.id)
|
||||||
|
else:
|
||||||
|
# If it's a single value or something else, make it a list
|
||||||
|
existing_shared_with = [existing_shared_with, default_attorney_user.id]
|
||||||
|
mutable_data["shared_with"] = existing_shared_with
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass # Default attorney not found, skip linking
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
271
dta_service/core/views/property.py
Normal file
271
dta_service/core/views/property.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
from rest_framework import viewsets, filters, generics, status
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
import requests
|
||||||
|
from core.models import (
|
||||||
|
Property,
|
||||||
|
PropertyOwner,
|
||||||
|
PropertyWalkScoreInfo,
|
||||||
|
PropertySave,
|
||||||
|
PropertyPictures,
|
||||||
|
)
|
||||||
|
from core.serializers import (
|
||||||
|
PropertyResponseSerializer,
|
||||||
|
PropertyRequestSerializer,
|
||||||
|
PropertySaveSerializer,
|
||||||
|
PropertyPictureSerializer,
|
||||||
|
)
|
||||||
|
from core.permissions import IsOwnerOrReadOnly
|
||||||
|
from core.filters import PropertyFilterSet
|
||||||
|
from core.services.property_description_generator import PropertyDescriptionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyViewSet(viewsets.ModelViewSet):
|
||||||
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||||
|
filterset_class = PropertyFilterSet
|
||||||
|
search_fields = ["address", "city", "state", "zip_code"]
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action in ["increment_view_count", "increment_save_count"]:
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
else:
|
||||||
|
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||||
|
return [permission() for permission in permission_classes]
|
||||||
|
|
||||||
|
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 PropertyResponseSerializer
|
||||||
|
return PropertyRequestSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
is_searching_others = bool(
|
||||||
|
self.request.query_params.get(filters.SearchFilter.search_param)
|
||||||
|
)
|
||||||
|
if user.user_type == "property_owner":
|
||||||
|
if is_searching_others:
|
||||||
|
return Property.objects.exclude(owner__user=user)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Property.objects.filter(owner__user=user)
|
||||||
|
|
||||||
|
return Property.objects.all()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
if self.request.user.user_type == "property_owner":
|
||||||
|
owner = PropertyOwner.objects.get(user=self.request.user)
|
||||||
|
## 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'}"
|
||||||
|
)
|
||||||
|
if res.ok:
|
||||||
|
data = res.json()
|
||||||
|
has_transit = data.get("transit")
|
||||||
|
has_bike = data.get("bike")
|
||||||
|
walk_score = PropertyWalkScoreInfo.objects.create(
|
||||||
|
walk_score=data.get("walkscore"),
|
||||||
|
walk_description=data.get("description"),
|
||||||
|
ws_link=data.get("ws_link"),
|
||||||
|
logo_url=data.get("logo_url"),
|
||||||
|
transit_score=data.get("transit").get("score")
|
||||||
|
if has_transit
|
||||||
|
else None,
|
||||||
|
transit_description=data.get("transit").get("description")
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
serializer.save(owner=owner)
|
||||||
|
else:
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def increment_view_count(self, request, pk=None):
|
||||||
|
property_obj = self.get_object()
|
||||||
|
property_obj.views += 1
|
||||||
|
property_obj.save()
|
||||||
|
# Create the user view model
|
||||||
|
# UserViewModel.objects.create(
|
||||||
|
# user__id=pk
|
||||||
|
# )
|
||||||
|
return Response({"views": property_obj.views}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def increment_save_count(self, request, pk=None):
|
||||||
|
property_obj = self.get_object()
|
||||||
|
property_obj.saves += 1
|
||||||
|
property_obj.save()
|
||||||
|
return Response({"saves": property_obj.saves}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyPictureViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||||
|
serializer_class = PropertyPictureSerializer
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyDescriptionView(generics.UpdateAPIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def put(self, request, property_id):
|
||||||
|
# check to make sure the property belongs to the user
|
||||||
|
properties = Property.objects.filter(owner__user=request.user, id=property_id)
|
||||||
|
if len(properties) == 0:
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
elif len([properties]) > 1:
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
else:
|
||||||
|
# generate the description
|
||||||
|
prop = properties.first()
|
||||||
|
generator = PropertyDescriptionGenerator()
|
||||||
|
description = generator.generate_response(prop)
|
||||||
|
print(description)
|
||||||
|
# save the description
|
||||||
|
prop.description = description
|
||||||
|
prop.save()
|
||||||
|
serializer = PropertyResponseSerializer(prop)
|
||||||
|
|
||||||
|
# return the description
|
||||||
|
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertySaveViewSet(
|
||||||
|
viewsets.mixins.CreateModelMixin,
|
||||||
|
viewsets.mixins.ListModelMixin,
|
||||||
|
viewsets.mixins.DestroyModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
A viewset that provides 'create', 'list', and 'destroy' actions
|
||||||
|
for saved properties.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PropertySave.objects.all()
|
||||||
|
serializer_class = PropertySaveSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
This view should return a list of all saved properties
|
||||||
|
for the currently authenticated user.
|
||||||
|
"""
|
||||||
|
user = self.request.user
|
||||||
|
return PropertySave.objects.filter(user=user).order_by("-created_at")
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""
|
||||||
|
Saves the new PropertySave instance, associating it with
|
||||||
|
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)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Unsaves a property.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
instance = self.get_object()
|
||||||
|
property = instance.property
|
||||||
|
property.saves -= 1
|
||||||
|
property.save()
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
return Response(
|
||||||
|
{"detail": "Property successfully unsaved."},
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
except PropertySave.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"detail": "PropertySave instance not found."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
API_KEY = "AIMLOPERATIONSLLC-a7ce-7525-b38f-cf8b4d52ca70"
|
||||||
|
|
||||||
|
|
||||||
|
class PropertDetailProxyView(APIView):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if not API_KEY:
|
||||||
|
return Response(
|
||||||
|
{"detail": "API KEY is missing"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
external_headers = {
|
||||||
|
"accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": API_KEY, # SENSITIVE KEY: Used on server-side only
|
||||||
|
"X-User-Id": "UniqueUserIdentifier",
|
||||||
|
}
|
||||||
|
payload = request.data
|
||||||
|
print(payload)
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
"https://api.realestateapi.com/v2/PropertyDetail",
|
||||||
|
headers=external_headers,
|
||||||
|
json=payload, # Send the data as JSON
|
||||||
|
)
|
||||||
|
print("we have a response")
|
||||||
|
print(response)
|
||||||
|
response.raise_for_status()
|
||||||
|
return Response(response.json(), status=response.status_code)
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{"detail": f"External API Error: {e.response.text}"},
|
||||||
|
status=e.response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteProxyView(APIView):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if not API_KEY:
|
||||||
|
return Response(
|
||||||
|
{"detail": "API KEY is missing"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1
|
||||||
|
external_headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": API_KEY, # SENSITIVE KEY: Used on server-side only
|
||||||
|
"X-User-Id": "UniqueUserIdentifier",
|
||||||
|
}
|
||||||
|
# 2. Extract data (e.g., search query) from the request sent by React
|
||||||
|
# You can forward the entire body or specific parameters
|
||||||
|
payload = request.data
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
"https://api.realestateapi.com/v2/AutoComplete",
|
||||||
|
headers=external_headers,
|
||||||
|
json=payload, # Send the data as JSON
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return Response(response.json(), status=response.status_code)
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{"detail": f"External API Error: {e.response.text}"},
|
||||||
|
status=e.response.status_code,
|
||||||
|
)
|
||||||
47
dta_service/core/views/property_info.py
Normal file
47
dta_service/core/views/property_info.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from rest_framework import viewsets, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from core.models import OpenHouse, Property
|
||||||
|
from core.serializers import OpenHouseSerializer
|
||||||
|
from core.permissions import IsPropertyOwner
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
23
dta_service/core/views/property_owner.py
Normal file
23
dta_service/core/views/property_owner.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from rest_framework import viewsets, filters
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from core.models import PropertyOwner
|
||||||
|
from core.serializers import PropertyOwnerSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyOwnerViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = PropertyOwnerSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||||
|
search_fields = ["user__first_name", "user__last_name", "user__email"]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if user.user_type == "property_owner":
|
||||||
|
if PropertyOwner.objects.filter(user=user).count() == 0:
|
||||||
|
return PropertyOwner.objects.create(
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
return PropertyOwner.objects.filter(user=user)
|
||||||
|
else:
|
||||||
|
return PropertyOwner.objects.all()
|
||||||
22
dta_service/core/views/real_estate_agent.py
Normal file
22
dta_service/core/views/real_estate_agent.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from core.models import RealEstateAgent
|
||||||
|
from core.serializers import RealEstateAgentSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class RealEstateAgentViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = RealEstateAgentSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
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 user.user_type == "real_estate_agent":
|
||||||
|
if not RealEstateAgent.objects.filter(user=user).exists():
|
||||||
|
return RealEstateAgent.objects.create(user=user)
|
||||||
|
return RealEstateAgent.objects.filter(user=user)
|
||||||
|
else:
|
||||||
|
return RealEstateAgent.objects.all()
|
||||||
64
dta_service/core/views/support.py
Normal file
64
dta_service/core/views/support.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from rest_framework import viewsets, generics
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from core.models import SupportCase, SupportMessage, FAQ
|
||||||
|
from core.serializers import (
|
||||||
|
SupportCaseListSerializer,
|
||||||
|
SupportCaseDetailSerializer,
|
||||||
|
SupportMessageSerializer,
|
||||||
|
FAQSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SupportCaseViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return SupportCase.objects.filter(user=self.request.user).order_by(
|
||||||
|
"-updated_at"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == "retrieve":
|
||||||
|
return SupportCaseDetailSerializer
|
||||||
|
return SupportCaseListSerializer
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def add_message(self, request, pk=None):
|
||||||
|
support_case = self.get_object()
|
||||||
|
serializer = SupportMessageSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(user=request.user, support_case=support_case)
|
||||||
|
return Response(serializer.data)
|
||||||
|
return Response(serializer.errors, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
class FAQListView(generics.ListAPIView):
|
||||||
|
queryset = FAQ.objects.all().order_by("order")
|
||||||
|
serializer_class = FAQSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class SupportMessageViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = SupportMessage.objects.all()
|
||||||
|
serializer_class = SupportMessageSerializer
|
||||||
|
permission_classes = [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 = [IsAuthenticated]
|
||||||
19
dta_service/core/views/support_agent.py
Normal file
19
dta_service/core/views/support_agent.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
from core.models import SupportAgent
|
||||||
|
from core.serializers import SupportAgentSerializer
|
||||||
|
from core.permissions import IsSupportAgent
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
127
dta_service/core/views/user.py
Normal file
127
dta_service/core/views/user.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from rest_framework import generics, permissions, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from django.db import transaction
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from core.serializers import (
|
||||||
|
CustomTokenObtainPairSerializer,
|
||||||
|
UserRegisterSerializer,
|
||||||
|
UserSerializer,
|
||||||
|
PasswordResetRequestSerializer,
|
||||||
|
PasswordResetConfirmSerializer,
|
||||||
|
)
|
||||||
|
from core.services.email_service import EmailService
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||||
|
serializer_class = CustomTokenObtainPairSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegisterView(generics.CreateAPIView):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserRegisterSerializer
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
authentication_classes = ()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
# Save the user instance
|
||||||
|
user = serializer.save()
|
||||||
|
|
||||||
|
# If the user is a vendor, create the associated Vendor model
|
||||||
|
from core.models.vendor import Vendor
|
||||||
|
vendor_type = self.request.data.get('vendor_type')
|
||||||
|
if user.user_type == 'vendor' and vendor_type:
|
||||||
|
# Create Vendor with basic info; business_name defaults to user's name
|
||||||
|
Vendor.objects.create(
|
||||||
|
user=user,
|
||||||
|
business_name=f"{user.first_name} {user.last_name}",
|
||||||
|
business_type=vendor_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate activation link (placeholder)
|
||||||
|
activation_link = "http://your-frontend-url.com/activate/"
|
||||||
|
try:
|
||||||
|
EmailService.send_registration_email(user, activation_link)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRetrieveView(generics.RetrieveAPIView, generics.UpdateAPIView):
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
serializer = UserSerializer(request.user)
|
||||||
|
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = UserSerializer(request.user, data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||||
|
else:
|
||||||
|
print(serializer.errors)
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
data=UserSerializer(request.user).data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSignTosView(generics.UpdateAPIView):
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def put(self, request):
|
||||||
|
user = User.objects.get(email=request.user.email)
|
||||||
|
user.tos_signed = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
serializer = UserSerializer(user)
|
||||||
|
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutView(APIView):
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
authentication_classes = ()
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
refresh_token = request.data["refresh_token"]
|
||||||
|
token = RefreshToken(refresh_token)
|
||||||
|
token.blacklist()
|
||||||
|
return Response(status=status.HTTP_205_RESET_CONTENT)
|
||||||
|
except Exception as e:
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetRequestView(APIView):
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = PasswordResetRequestSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(
|
||||||
|
{"detail": "Password reset email has been sent."},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetConfirmView(APIView):
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = PasswordResetConfirmSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(
|
||||||
|
{"detail": "Password has been reset successfully."},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
68
dta_service/core/views/vendor.py
Normal file
68
dta_service/core/views/vendor.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from rest_framework import viewsets, filters, generics, status
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from core.models import Vendor, UserViewModel
|
||||||
|
from core.serializers import VendorSerializer
|
||||||
|
from core.permissions import IsOwnerOrReadOnly
|
||||||
|
from core.filters import VendorFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class VendorViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = VendorSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||||
|
filterset_class = VendorFilterSet
|
||||||
|
search_fields = [
|
||||||
|
"business_name",
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"user__email",
|
||||||
|
]
|
||||||
|
filterset_fields = ["business_type"]
|
||||||
|
lookup_field = "user__id" # or 'user__id' if you want to be explicit
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action in ["increment_view_count", "increment_save_count"]:
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
else:
|
||||||
|
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||||
|
return [permission() for permission in permission_classes]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# Your existing logic is fine here
|
||||||
|
user = self.request.user
|
||||||
|
if user.user_type == "vendor":
|
||||||
|
# If the Vendor profile doesn't exist, create it
|
||||||
|
if not Vendor.objects.filter(user=user).exists():
|
||||||
|
return Vendor.objects.create(user=user)
|
||||||
|
return Vendor.objects.filter(user=user)
|
||||||
|
return Vendor.objects.all()
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
# Override get_object to ensure the user can only access their own Vendor profile
|
||||||
|
# when a specific ID is provided in the URL.
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
obj = generics.get_object_or_404(queryset, user=self.request.user)
|
||||||
|
self.check_object_permissions(self.request, obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
# The update method will now handle the vendor profile correctly
|
||||||
|
# and ignore any user data in the payload.
|
||||||
|
partial = kwargs.pop("partial", False)
|
||||||
|
instance = self.get_object()
|
||||||
|
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
self.perform_update(serializer)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def increment_view_count(self, request, user__id=None):
|
||||||
|
vendor_obj = Vendor.objects.get(user__id=user__id)
|
||||||
|
vendor_obj.views += 1
|
||||||
|
vendor_obj.save()
|
||||||
|
UserViewModel.objects.create(user_id=user__id)
|
||||||
|
|
||||||
|
return Response({"views": vendor_obj.views}, status=status.HTTP_200_OK)
|
||||||
42
dta_service/core/views/video.py
Normal file
42
dta_service/core/views/video.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from rest_framework import viewsets, filters
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from core.models import VideoCategory, Video, UserVideoProgress
|
||||||
|
from core.serializers import (
|
||||||
|
VideoCategorySerializer,
|
||||||
|
VideoSerializer,
|
||||||
|
UserVideoProgressSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoCategoryViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = VideoCategory.objects.all()
|
||||||
|
serializer_class = VideoCategorySerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class VideoViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Video.objects.all()
|
||||||
|
serializer_class = VideoSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||||
|
search_fields = ["title", "description"]
|
||||||
|
filterset_fields = ["category"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserVideoProgressViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = UserVideoProgressSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# first make sure that there is a progress for each video
|
||||||
|
videos = Video.objects.all()
|
||||||
|
for video in videos:
|
||||||
|
UserVideoProgress.objects.get_or_create(
|
||||||
|
user=self.request.user,
|
||||||
|
video=video,
|
||||||
|
)
|
||||||
|
return UserVideoProgress.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(user=self.request.user)
|
||||||
31
dta_service/tests/test_admin_forms.py
Normal file
31
dta_service/tests/test_admin_forms.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from core.models.support_agent import SupportAgent
|
||||||
|
from core.forms import SupportAgentCreationForm
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class SupportAgentCreationFormTest(TestCase):
|
||||||
|
def test_create_support_agent_form(self):
|
||||||
|
form_data = {
|
||||||
|
"email": "support@example.com",
|
||||||
|
"password": "securepassword123",
|
||||||
|
"first_name": "Support",
|
||||||
|
"last_name": "Agent",
|
||||||
|
}
|
||||||
|
form = SupportAgentCreationForm(data=form_data)
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
support_agent = form.save()
|
||||||
|
|
||||||
|
# Verify User creation
|
||||||
|
user = User.objects.get(email="support@example.com")
|
||||||
|
self.assertEqual(user.user_type, "support_agent")
|
||||||
|
self.assertTrue(user.check_password("securepassword123"))
|
||||||
|
self.assertEqual(user.first_name, "Support")
|
||||||
|
self.assertEqual(user.last_name, "Agent")
|
||||||
|
|
||||||
|
# Verify SupportAgent creation
|
||||||
|
self.assertTrue(SupportAgent.objects.filter(user=user).exists())
|
||||||
|
self.assertEqual(support_agent.user, user)
|
||||||
Reference in New Issue
Block a user