diff --git a/dta_service/core/admin.py b/dta_service/core/admin.py index 3c24a21..f4980a0 100644 --- a/dta_service/core/admin.py +++ b/dta_service/core/admin.py @@ -304,6 +304,14 @@ class VendorPicturesAdmin(admin.ModelAdmin): @admin.register(SupportAgent) class SupportAgentAdmin(admin.ModelAdmin): 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) diff --git a/dta_service/core/forms.py b/dta_service/core/forms.py new file mode 100644 index 0000000..2d42583 --- /dev/null +++ b/dta_service/core/forms.py @@ -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 diff --git a/dta_service/core/migrations/0033_alter_document_document_type_and_more.py b/dta_service/core/migrations/0033_alter_document_document_type_and_more.py new file mode 100644 index 0000000..b4677a2 --- /dev/null +++ b/dta_service/core/migrations/0033_alter_document_document_type_and_more.py @@ -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", + ), + ), + ], + ), + ] diff --git a/dta_service/core/migrations/0034_create_default_attorney.py b/dta_service/core/migrations/0034_create_default_attorney.py new file mode 100644 index 0000000..933dd91 --- /dev/null +++ b/dta_service/core/migrations/0034_create_default_attorney.py @@ -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), + ] diff --git a/dta_service/core/models.py b/dta_service/core/models.py deleted file mode 100644 index 61fbf17..0000000 --- a/dta_service/core/models.py +++ /dev/null @@ -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) diff --git a/dta_service/core/models/__init__.py b/dta_service/core/models/__init__.py new file mode 100644 index 0000000..8c84a70 --- /dev/null +++ b/dta_service/core/models/__init__.py @@ -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 diff --git a/dta_service/core/models/attorney.py b/dta_service/core/models/attorney.py new file mode 100644 index 0000000..637b20d --- /dev/null +++ b/dta_service/core/models/attorney.py @@ -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})" diff --git a/dta_service/core/models/bid.py b/dta_service/core/models/bid.py new file mode 100644 index 0000000..67093b9 --- /dev/null +++ b/dta_service/core/models/bid.py @@ -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}" diff --git a/dta_service/core/models/conversation.py b/dta_service/core/models/conversation.py new file mode 100644 index 0000000..dbf9c59 --- /dev/null +++ b/dta_service/core/models/conversation.py @@ -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}" diff --git a/dta_service/core/models/document.py b/dta_service/core/models/document.py new file mode 100644 index 0000000..1641518 --- /dev/null +++ b/dta_service/core/models/document.py @@ -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}" diff --git a/dta_service/core/models/property.py b/dta_service/core/models/property.py new file mode 100644 index 0000000..dfdae55 --- /dev/null +++ b/dta_service/core/models/property.py @@ -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") diff --git a/dta_service/core/models/property_info.py b/dta_service/core/models/property_info.py new file mode 100644 index 0000000..638ddb9 --- /dev/null +++ b/dta_service/core/models/property_info.py @@ -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) diff --git a/dta_service/core/models/property_owner.py b/dta_service/core/models/property_owner.py new file mode 100644 index 0000000..3a30ebf --- /dev/null +++ b/dta_service/core/models/property_owner.py @@ -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() diff --git a/dta_service/core/models/real_estate_agent.py b/dta_service/core/models/real_estate_agent.py new file mode 100644 index 0000000..25c642b --- /dev/null +++ b/dta_service/core/models/real_estate_agent.py @@ -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})" diff --git a/dta_service/core/models/support.py b/dta_service/core/models/support.py new file mode 100644 index 0000000..3388815 --- /dev/null +++ b/dta_service/core/models/support.py @@ -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) diff --git a/dta_service/core/models/support_agent.py b/dta_service/core/models/support_agent.py new file mode 100644 index 0000000..b8ebc57 --- /dev/null +++ b/dta_service/core/models/support_agent.py @@ -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) diff --git a/dta_service/core/models/user.py b/dta_service/core/models/user.py new file mode 100644 index 0000000..21493eb --- /dev/null +++ b/dta_service/core/models/user.py @@ -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}" diff --git a/dta_service/core/models/vendor.py b/dta_service/core/models/vendor.py new file mode 100644 index 0000000..3c22281 --- /dev/null +++ b/dta_service/core/models/vendor.py @@ -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) diff --git a/dta_service/core/models/video.py b/dta_service/core/models/video.py new file mode 100644 index 0000000..179e847 --- /dev/null +++ b/dta_service/core/models/video.py @@ -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) diff --git a/dta_service/core/serializers.py b/dta_service/core/serializers.py deleted file mode 100644 index 5704048..0000000 --- a/dta_service/core/serializers.py +++ /dev/null @@ -1,1215 +0,0 @@ -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 .models import ( - PropertyOwner, - SupportAgent, - Vendor, - Property, - VideoCategory, - Video, - UserVideoProgress, - Conversation, - Message, - PasswordResetToken, - OfferDocument, - PropertyPictures, - OpenHouse, - PropertySaleInfo, - PropertyTaxInfo, - PropertyWalkScoreInfo, - SchoolInfo, - Bid, - BidImage, - BidResponse, - RealEstateAgent, - Attorney, - UserViewModel, - PropertySave, - Document, - SellerDisclosure, - HomeImprovementReceipt, - SupportCase, - SupportMessage, - FAQ, -) -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 -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) - - class Meta: - model = User - fields = [ - "email", - "first_name", - "last_name", - "user_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): - validated_data.pop("password2") - user = User.objects.create_user(**validated_data) - return user - - -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 - - -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 - - -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 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) - - -class SubDocumentField(serializers.RelatedField): - def to_representation(self, value): - from .serializers import ( - OfferSerializer, - SellerDisclosureSerializer, - HomeImprovementReceiptSerializer, - ) - - # Check for each related type and use the correct serializer - if hasattr(value, "offer_data"): - return OfferSerializer(value.offer_data).data - elif hasattr(value, "seller_disclosure_data"): - return SellerDisclosureSerializer(value.seller_disclosure_data).data - elif hasattr(value, "home_improvement_receipt_data"): - return HomeImprovementReceiptSerializer( - value.home_improvement_receipt_data - ).data - return None - - -class DocumentSerializer(serializers.ModelSerializer): - sub_document = SubDocumentField(source="*", read_only=True) - shared_with = serializers.PrimaryKeyRelatedField( - queryset=User.objects.all(), many=True, required=False - ) # Allow sharing - - class Meta: - model = Document - fields = [ - "id", - "property", - "file", - "document_type", - "description", - "uploaded_by", - "shared_with", - "created_at", - "updated_at", - "sub_document", - ] - read_only_fields = ["id", "created_at", "updated_at"] - - -class SchoolInfoSerializer(serializers.ModelSerializer): - class 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 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) - - 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": - # Set listed_date to current time - validated_data["listed_date"] = timezone.now() - elif new_status == "off_market": - # Set listed_date to null - validated_data["listed_date"] = None - - # Handle nested updates - schools_data = validated_data.pop("schools", None) - tax_info_data = validated_data.pop("tax_info", None) - sale_info_data = validated_data.pop("sale_info", None) - walk_score_data = validated_data.pop("walk_score", None) - - # Update the main Property instance - instance = super().update(instance, validated_data) - - # Handle nested updates (e.g., update or create new nested objects) - if tax_info_data: - tax_info_instance, created = PropertyTaxInfo.objects.update_or_create( - property=instance, defaults=tax_info_data - ) - - if walk_score_data: - walk_score_instance, created = WalkScore.objects.update_or_create( - property=instance, defaults=walk_score_data - ) - - # For "many" relationships like schools and sale_info, you might need more complex logic - # (e.g., clearing old objects and creating new ones, or matching by ID) - if schools_data is not None: - # Example: Clear existing schools and create new ones - instance.schools.all().delete() - for school_data in schools_data: - SchoolInfo.objects.create(property=instance, **school_data) - - if sale_info_data is not None: - instance.sale_info.all().delete() - for sale_data in sale_info_data: - PropertySaleInfo.objects.create(property=instance, **sale_data) - - return instance - - -class VideoCategorySerializer(serializers.ModelSerializer): - class 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 - - -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"] - - -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 - - -# class OfferRequestSerializer(serializers.ModelSerializer): -# previous_offer = serializers.PrimaryKeyRelatedField(read_only=True) - -# class Meta: -# model = Offer -# fields = ["id", "user", "property", "status", "previous_offer", "is_active"] -# read_only_fields = ["id", "created_at", "updated_at"] - -# def get_previous_offer(self, model_field): -# return OfferRequestSerializer() - - -# class OfferResponseSerializer(serializers.ModelSerializer): -# previous_offer = serializers.PrimaryKeyRelatedField(read_only=True) -# user = UserSerializer() -# property = PropertyResponseSerializer() - -# class Meta: -# model = Offer -# fields = [ -# "id", -# "user", -# "property", -# "status", -# "previous_offer", -# "is_active", -# "created_at", -# "updated_at", -# ] -# read_only_fields = ["id", "created_at", "updated_at"] - -# def get_previous_offer(self, model_field): -# return OfferResponseSerializer() - -# def validate_status(self, value): -# return value - - -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 - - -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 - - -class SupportAgentSerializer(serializers.ModelSerializer): - user = UserSerializer(read_only=True) - - class Meta: - model = SupportAgent - fields = [ - "user", - ] - read_only_fields = ["created_at", "updated_at"] - - def create(self, validated_data): - user_instance = self.context.get("user") - if not user_instance: - raise serializers.ValidationError( - "User instance must be provided to create a SupportAgent." - ) - - # Ensure the user_type is correctly set for the new user - if user_instance.user_type != "support_agent": - user_instance.user_type = "support_agent" - user_instance.save() - - agent = SupportAgent.objects.create(user=user_instance, **validated_data) - return agent - - -class RealEstateAgentSerializer(serializers.ModelSerializer): - 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 - - -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 - - -class OfferSerializer(serializers.ModelSerializer): - # parent_offer = 'self' - parent_offer = serializers.SerializerMethodField() - - class Meta: - model = OfferDocument - - # 'document' field is excluded as it's set by the view after creating the generic Document - exclude = ["document"] - read_only_fields = ["parent_offer"] - - def get_parent_offer(self, obj): - if obj.parent_offer: - # Use the same serializer recursively for the parent offer - return OfferSerializer(obj.parent_offer).data - return None - - -class SellerDisclosureSerializer(serializers.ModelSerializer): - class Meta: - model = SellerDisclosure - fields = [ - "general_defects", - "roof_condition", - "roof_age", - "known_roof_leaks", - "plumbing_issues", - "electrical_issues", - "hvac_condition", - "hvac_age", - "known_lead_paint", - "known_asbestos", - "known_radon", - "past_water_damage", - "structural_issues", - "neighborhood_nuisances", - "property_line_disputes", - "appliances_included", - ] - # exclude = ['document'] - - -class HomeImprovementReceiptSerializer(serializers.ModelSerializer): - class Meta: - model = HomeImprovementReceipt - exclude = ["document"] - - -class SupportMessageSerializer(serializers.ModelSerializer): - user_first_name = serializers.CharField(source="user.first_name", read_only=True) - user_last_name = serializers.CharField(source="user.last_name", read_only=True) - - class Meta: - model = SupportMessage - fields = [ - "id", - "text", - "support_case", - "user", - "created_at", - "updated_at", - "user_first_name", - "user_last_name", - ] - read_only_fields = ["created_at", "updated_at", "user"] - - -class SupportCaseListSerializer(serializers.ModelSerializer): - class Meta: - model = SupportCase - fields = [ - "id", - "title", - "description", - "category", - "status", - "user", - "updated_at", - ] - read_only_fields = ["created_at", "updated_at", "user"] - - -class SupportCaseDetailSerializer(serializers.ModelSerializer): - messages = SupportMessageSerializer(many=True, read_only=True) - - class Meta: - model = SupportCase - fields = [ - "id", - "title", - "description", - "category", - "status", - "user", - "messages", - ] - read_only_fields = ["created_at", "updated_at", "user"] - - -class FAQSerializer(serializers.ModelSerializer): - class Meta: - model = FAQ - fields = ["order", "question", "answer"] diff --git a/dta_service/core/serializers/__init__.py b/dta_service/core/serializers/__init__.py new file mode 100644 index 0000000..84e7d95 --- /dev/null +++ b/dta_service/core/serializers/__init__.py @@ -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, +) diff --git a/dta_service/core/serializers/attorney.py b/dta_service/core/serializers/attorney.py new file mode 100644 index 0000000..22182b3 --- /dev/null +++ b/dta_service/core/serializers/attorney.py @@ -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 diff --git a/dta_service/core/serializers/bid.py b/dta_service/core/serializers/bid.py new file mode 100644 index 0000000..0906e27 --- /dev/null +++ b/dta_service/core/serializers/bid.py @@ -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 diff --git a/dta_service/core/serializers/conversation.py b/dta_service/core/serializers/conversation.py new file mode 100644 index 0000000..e736efd --- /dev/null +++ b/dta_service/core/serializers/conversation.py @@ -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"] diff --git a/dta_service/core/serializers/document.py b/dta_service/core/serializers/document.py new file mode 100644 index 0000000..8f2f5dc --- /dev/null +++ b/dta_service/core/serializers/document.py @@ -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"] diff --git a/dta_service/core/serializers/property.py b/dta_service/core/serializers/property.py new file mode 100644 index 0000000..07b82c2 --- /dev/null +++ b/dta_service/core/serializers/property.py @@ -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 diff --git a/dta_service/core/serializers/property_info.py b/dta_service/core/serializers/property_info.py new file mode 100644 index 0000000..baf396a --- /dev/null +++ b/dta_service/core/serializers/property_info.py @@ -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) diff --git a/dta_service/core/serializers/property_owner.py b/dta_service/core/serializers/property_owner.py new file mode 100644 index 0000000..373c2bc --- /dev/null +++ b/dta_service/core/serializers/property_owner.py @@ -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 diff --git a/dta_service/core/serializers/real_estate_agent.py b/dta_service/core/serializers/real_estate_agent.py new file mode 100644 index 0000000..7fc30b3 --- /dev/null +++ b/dta_service/core/serializers/real_estate_agent.py @@ -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 diff --git a/dta_service/core/serializers/support.py b/dta_service/core/serializers/support.py new file mode 100644 index 0000000..ba7a4fe --- /dev/null +++ b/dta_service/core/serializers/support.py @@ -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"] diff --git a/dta_service/core/serializers/support_agent.py b/dta_service/core/serializers/support_agent.py new file mode 100644 index 0000000..f5fcdda --- /dev/null +++ b/dta_service/core/serializers/support_agent.py @@ -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 diff --git a/dta_service/core/serializers/user.py b/dta_service/core/serializers/user.py new file mode 100644 index 0000000..52ee259 --- /dev/null +++ b/dta_service/core/serializers/user.py @@ -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 diff --git a/dta_service/core/serializers/vendor.py b/dta_service/core/serializers/vendor.py new file mode 100644 index 0000000..8d54b41 --- /dev/null +++ b/dta_service/core/serializers/vendor.py @@ -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 diff --git a/dta_service/core/serializers/video.py b/dta_service/core/serializers/video.py new file mode 100644 index 0000000..d0b11c2 --- /dev/null +++ b/dta_service/core/serializers/video.py @@ -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 diff --git a/dta_service/core/services/document_service.py b/dta_service/core/services/document_service.py new file mode 100644 index 0000000..076cfcf --- /dev/null +++ b/dta_service/core/services/document_service.py @@ -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 diff --git a/dta_service/core/tests/test_document_service.py b/dta_service/core/tests/test_document_service.py new file mode 100644 index 0000000..12adac7 --- /dev/null +++ b/dta_service/core/tests/test_document_service.py @@ -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)) diff --git a/dta_service/core/tests/test_document_signing.py b/dta_service/core/tests/test_document_signing.py new file mode 100644 index 0000000..b5836a5 --- /dev/null +++ b/dta_service/core/tests/test_document_signing.py @@ -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.") diff --git a/dta_service/core/tests/test_property_serializer.py b/dta_service/core/tests/test_property_serializer.py new file mode 100644 index 0000000..4adaef3 --- /dev/null +++ b/dta_service/core/tests/test_property_serializer.py @@ -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) diff --git a/dta_service/core/urls/support.py b/dta_service/core/urls/support.py index 755bdcf..4ad6350 100644 --- a/dta_service/core/urls/support.py +++ b/dta_service/core/urls/support.py @@ -3,9 +3,9 @@ from rest_framework.routers import DefaultRouter from core.views import SupportCaseViewSet, SupportMessageViewSet, FAQViewSet router = DefaultRouter() -router.register(r"cases", SupportCaseViewSet) -router.register(r"messages", SupportMessageViewSet) -router.register(r"faq", FAQViewSet) +router.register(r"cases", SupportCaseViewSet, basename="support-case") +router.register(r"messages", SupportMessageViewSet, basename="support-message") +router.register(r"faq", FAQViewSet, basename="faq") urlpatterns = [ path("", include(router.urls)), diff --git a/dta_service/core/views.py b/dta_service/core/views.py deleted file mode 100644 index c52fbed..0000000 --- a/dta_service/core/views.py +++ /dev/null @@ -1,1135 +0,0 @@ -from rest_framework import generics, permissions, status, viewsets -from rest_framework.response import Response -from rest_framework.views import APIView -from django.db import transaction -from rest_framework_simplejwt.views import TokenObtainPairView -from rest_framework_simplejwt.tokens import RefreshToken -import requests -from rest_framework.decorators import action -from django.contrib.auth import get_user_model -from django.shortcuts import get_object_or_404 -from .models import ( - PropertyOwner, - Vendor, - SupportAgent, - Property, - VideoCategory, - Video, - UserVideoProgress, - Conversation, - Message, - OfferDocument, - PropertyWalkScoreInfo, - PropertyTaxInfo, - SchoolInfo, - Bid, - BidResponse, - Attorney, - RealEstateAgent, - UserViewModel, - PropertySave, - Document, - SellerDisclosure, - HomeImprovementReceipt, - OfferDocument, - OpenHouse, - SupportCase, - SupportMessage, - FAQ, -) -from .serializers import ( - CustomTokenObtainPairSerializer, - SupportAgentSerializer, - UserSerializer, - UserRegisterSerializer, - PropertyOwnerSerializer, - VendorSerializer, - PropertyResponseSerializer, - PropertyRequestSerializer, - VideoCategorySerializer, - VideoSerializer, - UserVideoProgressSerializer, - ConversationRequestSerializer, - ConversationResponseSerializer, - MessageSerializer, - PasswordResetRequestSerializer, - PasswordResetConfirmSerializer, - # OfferRequestSerializer, - # OfferResponseSerializer, - PropertyPictureSerializer, - BidSerializer, - BidResponseSerializer, - AttorneySerializer, - RealEstateAgentSerializer, - PropertySaveSerializer, - DocumentSerializer, - OfferSerializer, - SellerDisclosureSerializer, - HomeImprovementReceiptSerializer, - OpenHouseSerializer, - SupportMessageSerializer, - SupportCaseDetailSerializer, - SupportCaseListSerializer, - FAQSerializer, -) -from rest_framework.permissions import IsAuthenticated -from .permissions import ( - IsOwnerOrReadOnly, - IsPropertyOwner, - IsVendor, - IsParticipant, - IsParticipantInOffer, - IsSupportAgent, - IsPropertyOwnerOrVendorOrAttorney, -) -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters -from django.db.models import Q, Prefetch -from .services.property_description_generator import PropertyDescriptionGenerator -from .filters import PropertyFilterSet, VendorFilterSet - -from .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 = () - - def perform_create(self, serializer): - # This will save the user instance and return the object - user = serializer.save() - - # Here you would typically generate an activation link. - # This is a placeholder as the exact link generation depends on your - # front-end URL and activation logic (e.g., using a token). - # For a full implementation, you would need to generate a unique token - # and include it in the URL. - # activation_link = "http://your-frontend-url.com/activate/" - - # Call the email-sending function with the newly created user object - # and the activation link. - try: - EmailService.send_registration_email(user, activation_link) - except Exception as e: - print(e) - - # You can optionally modify the response data here if needed. - # For example, to not return all user data. - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class UserRetrieveView(generics.RetrieveAPIView, generics.UpdateAPIView): - permission_classes = [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 = [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 OpenHouseViewSet(viewsets.ModelViewSet): - """ - A ViewSet for viewing and editing OpenHouse instances. - """ - - queryset = OpenHouse.objects.all() - serializer_class = OpenHouseSerializer - permission_classes = [IsAuthenticated, IsPropertyOwner] - - def get_queryset(self): - """ - This view should return a list of all the open houses - for the currently authenticated property owner. - """ - user = self.request.user - if user.is_authenticated and hasattr(user, "propertyowner"): - return OpenHouse.objects.filter( - property__owner=user.propertyowner - ).order_by("start_time") - return OpenHouse.objects.none() - - def perform_create(self, serializer): - """ - Ensures that the property being assigned to the open house belongs - to the authenticated property owner. - """ - property_id = self.request.data.get("property") - try: - property_instance = Property.objects.get( - id=property_id, owner__user=self.request.user - ) - serializer.save(property=property_instance) - except Property.DoesNotExist: - return Response( - { - "detail": "You do not have permission to schedule an open house for this property." - }, - status=status.HTTP_403_FORBIDDEN, - ) - - -class PasswordResetRequestView(APIView): - 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) - - -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() - - -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) - - -# Attorney ViewSet -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() - - -# Real Estate Agent ViewSet -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() - - -class SupportAgentViewSet(viewsets.ModelViewSet): - serializer_class = SupportAgentSerializer - permission_classes = [IsSupportAgent] - - def perform_create(self, serializer): - # Link to the currently authenticated user - serializer.save(user=self.request.user) - - def get_queryset(self): - user = self.request.user - if not SupportAgent.objects.filter(user=user).exists(): - return SupportAgent.objects.create(user=user) - return SupportAgent.objects.filter(user=user) - - -class PropertyViewSet(viewsets.ModelViewSet): - 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 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) - - -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 - - -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") - - -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, - ) - - -class DocumentViewSet(viewsets.ModelViewSet): - serializer_class = DocumentSerializer - permission_classes = [IsAuthenticated, IsPropertyOwnerOrVendorOrAttorney] - search_fields = [ - "document_type", - ] - - def get_queryset(self): - # Only allow users to see documents for properties they are associated with. - user = self.request.user - if user.user_type == "property_owner": - # Filter for documents the user can see - # documents = Document.objects.filter( - # Q(property__owner__user=user) | Q(uploaded_by=user) | Q(shared_with=user) - # )#.exclude(offer_data__parent_offer__isnull=False) - - # # Prefetch related OfferDocument data - # offers = OfferDocument.objects.filter(parent_offer__isnull=True) - # offers_prefetch = Prefetch('offer_data', queryset=offers) - # documents = documents.prefetch_related(offers_prefetch) - - return Document.objects.filter( - ( - Q(property__owner__user=user) - | Q(uploaded_by=user) - | Q(shared_with=user) - ) - & ( - Q(offer_data__isnull=True) - | Q(offer_data__counter_offers__isnull=True) - ) - ).distinct() - # Add more logic here for vendors and attorneys to see relevant documents - # For example, for an attorney associated with a property: - # return Document.objects.filter(property__attorney__user=user) - # For now, let's keep it simple. - return Document.objects.none() - - def perform_create(self, serializer): - # Automatically link the uploader - serializer.save(uploaded_by=self.request.user) - - -class RetrieveDocumentView(generics.RetrieveAPIView, generics.UpdateAPIView): - queryset = Document.objects.all() - serializer_class = DocumentSerializer - permission_classes = [IsAuthenticated] - lookup_field = "id" - - def get_object(self): - # Override get_object to use a query parameter. - document_id = self.request.query_params.get("docId") - if not document_id: - raise NotFound(detail="docId is required.") - - try: - # We use select_related to eagerly load the related documents - # to prevent extra database queries in the serializer. - return Document.objects.select_related( - "offer_data", "seller_disclosure_data", "home_improvement_receipt_data" - ).get(id=document_id) - except Document.DoesNotExist: - raise NotFound(detail="Document not found.") - - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer(instance) - # Return the data directly from the serializer. - return Response(serializer.data) - - # def get(self, request, *args, **kwargs): - # print(request, args, kwargs) - # documentId = request.query_params.get('docId', None) - # if not documentId: - # return Response( - # {"detail": "docId is required."}, - # status=status.HTTP_400_BAD_REQUEST - # ) - # try: - # document = Document.objects.get(id=documentId) - # document_serializer = DocumentSerializer(document) - # documentType = document.document_type - # if documentType == 'seller_disclosure': - - # serializer = SellerDisclosureSerializer(SellerDisclosure.objects.get(document_id=documentId)) - # elif documentType == 'offer_letter': - # serializer = OfferSerializer(OfferDocument.objects.get(document_id=documentId)) - # elif documentType == 'home_improvement_receipt': - # serializer = HomeImprovementReceiptSerializer(HomeImprovementReceipt.objects.get(document_id=documentId)) - # else: - # return Response({'error': 'couldnt find the document'}, status=status.HTTP_400_BAD_REQUEST) - - # data = { - # "document": document_serializer.data - # } - # data['document']['sub_document'] = serializer.data - - # return Response(data=data, status=status.HTTP_200_OK) - # except Exception as e: - # return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - def patch(self, request, *args, **kwargs): - print(request, args, kwargs) - document_id = request.data.get("document_id") - action = request.data.get("action") - if not document_id or not action: - return Response( - {"error": "Need to supply both document_id and action"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - try: - with transaction.atomic(): - document = Document.objects.get(id=document_id) - if document.document_type == "offer_letter": - offer_document = OfferDocument.objects.get(document__id=document_id) - if action == "accept": - # find all of the other documents that pertain to this property and reject them - other_docs = Document.objects.filter( - property=document.property, document_type="offer_letter" - ).exclude(id=document_id) - for other_doc in other_docs: - offer_doc = OfferDocument.objects.get( - document__id=other_doc.id - ) - offer_doc.status = "rejected" - offer_doc.save() - - offer_document.status = "accepted" - offer_document.save() - - elif action == "reject": - offer_document.status = "rejected" - offer_document.save() - - elif action == "counter": - offer_document.status = "countered" - offer_document.save() - - # Create a new generic Document for the counter-offer - - new_doc = Document.objects.create( - property=document.property, - document_type="offer_letter", - description=f"Counter-offer to document ID {document.id}", - uploaded_by=request.user, - shared_with=[document.uploaded_by], - ) - # Create the new OfferDocument, linking it to the new generic document - # and the parent offer. - new_offer_doc = OfferDocument.objects.create( - document=new_doc, - parent_offer=offer_document, - offer_price=Decimal(str(new_price)), - closing_date=new_closing_date, - closing_days=new_closing_days, - contingencies=new_contingencies or "", - status="pending", # Set the status of the new offer - ) - - # Return the newly created offer data - serializer = OfferSerializer(new_offer_doc) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - else: - return Response( - {"error": f"Dont understand the action: {action}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - else: - return Response( - {"error": "functionality is not implemented"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - return Response(data={}, status=status.HTTP_200_OK) - except Exception as e: - return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - - -class CreateDocumentView(APIView): - # Apply authentication if needed. For example, IsAuthenticated will ensure only logged-in users can upload. - permission_classes = [IsAuthenticated] - - def post(self, request, *args, **kwargs): - # Validate property_id and document_type first - property_id = request.data.get("property") - document_type = request.data.get("document_type") - - # Ensure property_id and document_type are provided - if not property_id: - return Response( - {"detail": "Property ID is required."}, - status=status.HTTP_400_BAD_REQUEST, - ) - if not document_type: - return Response( - {"detail": "Document type is required."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - property = Property.objects.get(id=property_id) - - # Check if the document_type is one of our specific types that require extra data - specific_document_types = { - "offer": OfferSerializer, - "seller_disclosure": SellerDisclosureSerializer, - "home_improvement_receipt": HomeImprovementReceiptSerializer, - } - - # Validate the generic Document data - # 'uploaded_by' can be automatically set to the current user - request.data["uploaded_by"] = ( - request.user.id if request.user.is_authenticated else None - ) - - # Initialize DocumentSerializer with all data, including file - # Make a mutable copy of request.data if it's an immutable QueryDict - mutable_data = request.data.copy() - print(mutable_data) - if mutable_data["document_type"] == "offer": - mutable_data["document_type"] = "offer_letter" - # smartly create the description - if document_type == "offer": - mutable_data["description"] = ( - f"${mutable_data['offer_price']} Offer for {property.address}" - ) - elif document_type == "seller_disclosure": - mutable_data["description"] = property.address - document_serializer = DocumentSerializer(data=mutable_data) - - with transaction.atomic(): - if document_serializer.is_valid(): - try: - # Save the generic Document instance - document = document_serializer.save() - - # If it's a specific type, validate and save its data - if document_type in specific_document_types: - specific_serializer_class = specific_document_types[ - document_type - ] - specific_data = ( - request.data.copy() - ) # Use a copy for specific serializer - - # Remove generic document fields that are not part of specific serializers - # This ensures the specific serializer only sees its relevant fields - for field in document_serializer.fields: - if field in specific_data: - del specific_data[field] - - specific_serializer = specific_serializer_class( - data=specific_data - ) - - if specific_serializer.is_valid(): - specific_instance = specific_serializer.save( - document=document - ) # Link to the document - # Prepare response data, including both generic and specific data - response_data = document_serializer.data - response_data[f"{document_type}_data"] = ( - specific_serializer.data - ) - return Response( - response_data, status=status.HTTP_201_CREATED - ) - else: - # If specific data is invalid, roll back the generic document creation - try: - # attempt to remove the document if it is already there - document.delete() - except: - pass - - return Response( - { - "detail": f"Invalid data for {document_type}.", - "errors": specific_serializer.errors, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - else: - # For generic document types (e.g., 'other', 'attorney_contract') - return Response( - document_serializer.data, status=status.HTTP_201_CREATED - ) - - except Exception as e: - # Catch any unexpected errors during transaction - return Response( - {"detail": f"An unexpected error occurred: {str(e)}"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - else: - # If generic document data is invalid - return Response( - document_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - - -class SupportCaseViewSet(viewsets.ModelViewSet): - queryset = SupportCase.objects.all() - permission_classes = [permissions.IsAuthenticated] - - def get_serializer_class(self): - if self.action == "retrieve": - return SupportCaseDetailSerializer - return SupportCaseListSerializer - - def get_queryset(self): - user = self.request.user - if user.user_type == "support_agent": - queryset = SupportCase.objects.all() - else: - queryset = SupportCase.objects.filter(user=user) - - status = self.request.query_params.get("status") - if status: - queryset = queryset.filter(status=status) - - category = self.request.query_params.get("category") - if category: - queryset = queryset.filter(category=category) - - return queryset - - def perform_create(self, serializer): - serializer.save(user=self.request.user) - - -class SupportMessageViewSet(viewsets.ModelViewSet): - queryset = SupportMessage.objects.all() - serializer_class = SupportMessageSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - user = self.request.user - if user.user_type == "support_agent": - return SupportMessage.objects.all() - return SupportMessage.objects.filter(support_case__user=user) - - def perform_create(self, serializer): - serializer.save(user=self.request.user) - - -class FAQViewSet(viewsets.ModelViewSet): - queryset = FAQ.objects.all() - serializer_class = FAQSerializer - permission_classes = [permissions.IsAuthenticated] - - -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, - ) diff --git a/dta_service/core/views/__init__.py b/dta_service/core/views/__init__.py new file mode 100644 index 0000000..85c7134 --- /dev/null +++ b/dta_service/core/views/__init__.py @@ -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, +) diff --git a/dta_service/core/views/attorney.py b/dta_service/core/views/attorney.py new file mode 100644 index 0000000..08dc75e --- /dev/null +++ b/dta_service/core/views/attorney.py @@ -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() diff --git a/dta_service/core/views/bid.py b/dta_service/core/views/bid.py new file mode 100644 index 0000000..6ab2fa4 --- /dev/null +++ b/dta_service/core/views/bid.py @@ -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") diff --git a/dta_service/core/views/conversation.py b/dta_service/core/views/conversation.py new file mode 100644 index 0000000..a0527a8 --- /dev/null +++ b/dta_service/core/views/conversation.py @@ -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 diff --git a/dta_service/core/views/document.py b/dta_service/core/views/document.py new file mode 100644 index 0000000..d7d3814 --- /dev/null +++ b/dta_service/core/views/document.py @@ -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 + ) diff --git a/dta_service/core/views/property.py b/dta_service/core/views/property.py new file mode 100644 index 0000000..feb3732 --- /dev/null +++ b/dta_service/core/views/property.py @@ -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, + ) diff --git a/dta_service/core/views/property_info.py b/dta_service/core/views/property_info.py new file mode 100644 index 0000000..aad9663 --- /dev/null +++ b/dta_service/core/views/property_info.py @@ -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, + ) diff --git a/dta_service/core/views/property_owner.py b/dta_service/core/views/property_owner.py new file mode 100644 index 0000000..54ce259 --- /dev/null +++ b/dta_service/core/views/property_owner.py @@ -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() diff --git a/dta_service/core/views/real_estate_agent.py b/dta_service/core/views/real_estate_agent.py new file mode 100644 index 0000000..3873023 --- /dev/null +++ b/dta_service/core/views/real_estate_agent.py @@ -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() diff --git a/dta_service/core/views/support.py b/dta_service/core/views/support.py new file mode 100644 index 0000000..380aebf --- /dev/null +++ b/dta_service/core/views/support.py @@ -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] diff --git a/dta_service/core/views/support_agent.py b/dta_service/core/views/support_agent.py new file mode 100644 index 0000000..43f42ae --- /dev/null +++ b/dta_service/core/views/support_agent.py @@ -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) diff --git a/dta_service/core/views/user.py b/dta_service/core/views/user.py new file mode 100644 index 0000000..828e7cd --- /dev/null +++ b/dta_service/core/views/user.py @@ -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) diff --git a/dta_service/core/views/vendor.py b/dta_service/core/views/vendor.py new file mode 100644 index 0000000..c4b28e0 --- /dev/null +++ b/dta_service/core/views/vendor.py @@ -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) diff --git a/dta_service/core/views/video.py b/dta_service/core/views/video.py new file mode 100644 index 0000000..022d680 --- /dev/null +++ b/dta_service/core/views/video.py @@ -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) diff --git a/dta_service/tests/test_admin_forms.py b/dta_service/tests/test_admin_forms.py new file mode 100644 index 0000000..03da9d4 --- /dev/null +++ b/dta_service/tests/test_admin_forms.py @@ -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)