big update

This commit is contained in:
2025-08-16 12:53:06 -05:00
parent 848d030b08
commit 2239c861b1
56 changed files with 5138 additions and 1296 deletions

1
.gitignore vendored
View File

@@ -257,3 +257,4 @@ local_settings.py
.env
db.sqlite3
media/

View File

@@ -1,3 +1,210 @@
from django.contrib import admin
from core.models import (
User,
PropertyOwner,
Message,
Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Offer,
PropertyPictures,
OpenHouse,
PropertySaleInfo,
PropertyTaxInfo,
PropertyWalkScoreInfo,
SchoolInfo,
Attorney, RealEstateAgent, UserViewModel, PropertySave
)
# Register your models here.
class UserAdmin(admin.ModelAdmin):
model = User
list_display = (
"email",
"first_name",
"last_name",
"is_active",
"is_staff",
"has_usable_password",
"user_type",
# "has_signed_tos",
"last_login",
)
search_fields = ("fields", "email", "first_name", "last_name", "user_type")
class PropertyOwnerAdmin(admin.ModelAdmin):
model = PropertyOwner
list_display = (
"pk",
"user",
"phone_number",
)
class VendorAdmin(admin.ModelAdmin):
model = Vendor
list_display = ("business_name", "business_type", "address")
search_fields = (
"business_name",
"business_type",
"phone_number",
"state",
"zip_code",
)
class VideoCategoryAdmin(admin.ModelAdmin):
model = VideoCategory
list_display = ("name", "description")
search_fields = ("name",)
class VideoAdmin(admin.ModelAdmin):
model = Video
list_display = ("title", "description", "category__name", "duration", "link")
search_fields = ("name",)
class UserVideoProgressAdmin(admin.ModelAdmin):
model = UserVideoProgress
class MessageStackedInline(admin.StackedInline):
model = Message
extra = 1
class ConversationAdmin(admin.ModelAdmin):
model = Conversation
inlines = [MessageStackedInline]
class OfferAdmin(admin.ModelAdmin):
model = Offer
list_display = ("pk", "user", "property", "status")
search_fields = ("user", "status")
class PropertyPicturesAdmin(admin.ModelAdmin):
model = PropertyPictures
list_display = ("id", "image")
class OpenHouseAdmin(admin.ModelAdmin):
model = OpenHouse
class PropertySaleInfoAdmin(admin.ModelAdmin):
model = PropertySaleInfo
class PropertyTaxInfoAdmin(admin.ModelAdmin):
model = PropertyTaxInfo
class PropertyWalkScoreInfoAdmin(admin.ModelAdmin):
model = PropertyWalkScoreInfo
class SchoolInfoAdmin(admin.ModelAdmin):
model = SchoolInfo
class PropertyWalkScoreInfoStackedInline(admin.StackedInline):
model = PropertyWalkScoreInfo
class SchoolInfoStackedInline(admin.StackedInline):
model = SchoolInfo
class PropertyAdmin(admin.ModelAdmin):
model = Property
list_display = ("pk", "owner", "address", "city", "state", "zip_code")
search_fields = ("address", "city", "state", "zip_code", "owner")
inlines = [PropertyWalkScoreInfoStackedInline, SchoolInfoStackedInline]
# Registering the new Attorney model
@admin.register(Attorney)
class AttorneyAdmin(admin.ModelAdmin):
list_display = ('user', 'firm_name', 'bar_number', 'phone_number', 'city', 'state', 'years_experience')
list_filter = ('state', 'years_experience', 'specialties') # You might need custom list_filter for JSONField
search_fields = ('user__email', 'user__first_name', 'user__last_name', 'firm_name', 'bar_number', 'city')
# Use filter_horizontal for JSONField if you want a nice interface for many-to-many like selection
# filter_horizontal = ('specialties', 'licensed_states')
fieldsets = (
(None, {
'fields': ('user', 'firm_name', 'bar_number', 'phone_number', 'address', 'city', 'state', 'zip_code', 'bio', 'profile_picture', 'website')
}),
('Professional Details', {
'fields': ('specialties', 'years_experience', 'licensed_states')
}),
('Location', {
'fields': ('latitude', 'longitude'),
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at')
# Registering the new RealEstateAgent model
@admin.register(RealEstateAgent)
class RealEstateAgentAdmin(admin.ModelAdmin):
list_display = ('user', 'brokerage_name', 'license_number', 'phone_number', 'city', 'state', 'agent_type', 'years_experience')
list_filter = ('agent_type', 'state', 'years_experience', 'specialties')
search_fields = ('user__email', 'user__first_name', 'user__last_name', 'brokerage_name', 'license_number', 'city')
#filter_horizontal = ('specialties', 'licensed_states')
fieldsets = (
(None, {
'fields': ('user', 'brokerage_name', 'license_number', 'phone_number', 'address', 'city', 'state', 'zip_code', 'bio', 'profile_picture', 'website', 'agent_type')
}),
('Professional Details', {
'fields': ('specialties', 'years_experience', 'licensed_states')
}),
('Location', {
'fields': ('latitude', 'longitude'),
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at')
@admin.register(UserViewModel)
class UserViewModelAdmin(admin.ModelAdmin):
list_display = ('pk','user','created_at')
readonly_fields = ('created_at',)
@admin.register(PropertySave)
class PropertySaveAdmin(admin.ModelAdmin):
list_display = ('pk', 'user','property')
readonly_fields = ('created_at',)
admin.site.register(User, UserAdmin)
admin.site.register(PropertyOwner, PropertyOwnerAdmin)
admin.site.register(Vendor, VendorAdmin)
admin.site.register(Property, PropertyAdmin)
admin.site.register(VideoCategory, VideoCategoryAdmin)
admin.site.register(Video, VideoAdmin)
admin.site.register(UserVideoProgress, UserVideoProgressAdmin)
admin.site.register(Conversation, ConversationAdmin)
admin.site.register(Offer, OfferAdmin)
admin.site.register(PropertyPictures, PropertyPicturesAdmin)
admin.site.register(OpenHouse, OpenHouseAdmin)
admin.site.register(PropertySaleInfo, PropertySaleInfoAdmin)
admin.site.register(PropertyTaxInfo, PropertyTaxInfoAdmin)
admin.site.register(PropertyWalkScoreInfo, PropertyWalkScoreInfoAdmin)
admin.site.register(SchoolInfo, SchoolInfoAdmin)

View File

@@ -2,5 +2,5 @@ from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'
default_auto_field = "django.db.models.BigAutoField"
name = "core"

View File

@@ -5,92 +5,119 @@ from django.contrib.auth import get_user_model
from rest_framework_simplejwt.tokens import AccessToken
from .models import Conversation, Message
from .serializers import MessageSerializer
from .services.moderation_classifier import moderation_classifier, ModerationLabel
from .services.llm_service import AsyncLLMService
User = get_user_model()
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.conversation_id = self.scope['url_route']['kwargs']['conversation_id']
self.conversation_group_name = f'chat_{self.conversation_id}'
print(self.scope)
self.account_id = self.scope["url_route"]["kwargs"]["account_id"]
self.account_group_name = f"chat_{self.account_id}"
# Authenticate user via JWT
token = self.scope.get('query_string').decode('utf-8').split('=')[1]
token = self.scope.get("query_string").decode("utf-8").split("=")[1]
try:
access_token = AccessToken(token)
user_id = access_token['user_id']
user_id = access_token["user_id"]
self.user = await self.get_user(user_id)
# Check if user is part of the conversation
conversation = await self.get_conversation(self.conversation_id)
if not await self.is_participant(conversation, self.user):
await self.close()
return
await self.channel_layer.group_add(
self.conversation_group_name,
self.channel_name
)
# # Check if user is part of the conversation
# conversation = await self.get_conversation(self.conversation_id)
# if not await self.is_participant(conversation, self.user):
# await self.close()
# return
#
# await self.channel_layer.group_add(
# self.account_group_name, self.account_id
# )
await self.accept()
except Exception as e:
print(f"Error: {e}")
await self.close()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.conversation_group_name,
self.channel_name
)
if (self.channel_layer):
await self.channel_layer.group_discard(
self.account_group_name, self.account_id
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# Save message to database
conversation = await self.get_conversation(self.conversation_id)
message_obj = await self.create_message(conversation, self.user, message)
# Serialize message
serializer = MessageSerializer(message_obj)
# Send message to room group
await self.channel_layer.group_send(
self.conversation_group_name,
{
'type': 'chat_message',
'message': serializer.data
}
)
print(text_data_json)
"""
First see if it is NSFW
Then to the conversation
"""
messages = text_data_json.get('messages')
moderation_result = await moderation_classifier.classify_async(messages[-1])
if moderation_result == ModerationLabel.NSFW:
await self.send('BEGINING_OF_THE_WORLD')
await self.send(str('Try again'))
await self.send('END_OF_THE_WORLD')
await self.send('BEGINING_OF_THE_WORLD')
service = AsyncLLMService()
response = ''
# get the account to add to the prompt
print('generating')
async for chunk in service.generate_response(
messages, self.user
):
response += chunk
await self.send(chunk)
print(response)
await self.send('END_OF_THE_WORLD')
# # Save message to database
# conversation = await self.get_conversation(self.conversation_id)
# message_obj = await self.create_message(conversation, self.user, message)
# # Serialize message
# serializer = MessageSerializer(message_obj)
# # Send message to room group
# await self.channel_layer.group_send(
# self.account_group_name,
# {"type": "chat_message", "message": serializer.data},
# )
async def chat_message(self, event):
message = event['message']
message = event["message"]
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
await self.send(text_data=json.dumps({"message": message}))
@database_sync_to_async
def get_user(self, user_id):
return User.objects.get(id=user_id)
@database_sync_to_async
def get_conversation(self, conversation_id):
return Conversation.objects.get(id=conversation_id)
@database_sync_to_async
def is_participant(self, conversation, user):
if user.user_type == 'property_owner':
if user.user_type == "property_owner":
return conversation.property_owner.user == user
elif user.user_type == 'vendor':
elif user.user_type == "vendor":
return conversation.vendor.user == user
return False
@database_sync_to_async
def create_message(self, conversation, user, text):
return Message.objects.create(
conversation=conversation,
sender=user,
text=text
)
return Message.objects.create(conversation=conversation, sender=user, text=text)

View File

@@ -0,0 +1,19 @@
import django_filters
from .models import Property
class PropertyFilterSet(django_filters.FilterSet):
address = django_filters.CharFilter(field_name='address', lookup_expr='icontains')
city = django_filters.CharFilter(field_name='city', lookup_expr='icontains')
state = django_filters.CharFilter(field_name='state', lookup_expr='icontains')
zip_code = django_filters.CharFilter(field_name='zip_code', lookup_expr='exact')
min_num_bedrooms = django_filters.NumberFilter(field_name='num_bedrooms', lookup_expr='gte')
max_num_bedrooms = django_filters.NumberFilter(field_name='num_bedrooms', lookup_expr='lte')
min_num_bathrooms = django_filters.NumberFilter(field_name='num_bathrooms', lookup_expr='gte')
max_num_bathrooms = django_filters.NumberFilter(field_name='num_bathrooms', lookup_expr='lte')
min_sq_ft = django_filters.NumberFilter(field_name='sq_ft', lookup_expr='gte')
max_sq_ft = django_filters.NumberFilter(field_name='sq_ft', lookup_expr='lte')
class Meta:
model = Property
fields = ['address', 'city', 'state', 'zip_code', 'min_num_bedrooms', 'max_num_bedrooms',
'min_num_bathrooms', 'max_num_bathrooms', 'min_sq_ft', 'max_sq_ft']

View File

@@ -13,154 +13,385 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name='User',
name="User",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', models.EmailField(max_length=254, unique=True)),
('first_name', models.CharField(max_length=30)),
('last_name', models.CharField(max_length=30)),
('user_type', models.CharField(choices=[('property_owner', 'Property Owner'), ('vendor', 'Vendor'), ('admin', 'Admin')], max_length=20)),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
("email", models.EmailField(max_length=254, unique=True)),
("first_name", models.CharField(max_length=30)),
("last_name", models.CharField(max_length=30)),
(
"user_type",
models.CharField(
choices=[
("property_owner", "Property Owner"),
("vendor", "Vendor"),
("admin", "Admin"),
],
max_length=20,
),
),
("is_active", models.BooleanField(default=True)),
("is_staff", models.BooleanField(default=False)),
(
"date_joined",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
migrations.CreateModel(
name='Conversation',
name="Conversation",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Property',
name="Property",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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)),
('market_value', models.DecimalField(decimal_places=2, max_digits=12)),
('loan_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
('loan_interest_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
('loan_term', models.IntegerField(blank=True, null=True)),
('loan_start_date', models.DateField(blank=True, null=True)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("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)),
("market_value", models.DecimalField(decimal_places=2, max_digits=12)),
(
"loan_amount",
models.DecimalField(
blank=True, decimal_places=2, max_digits=12, null=True
),
),
(
"loan_interest_rate",
models.DecimalField(
blank=True, decimal_places=2, max_digits=5, null=True
),
),
("loan_term", models.IntegerField(blank=True, null=True)),
("loan_start_date", models.DateField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='VideoCategory',
name="VideoCategory",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True, null=True)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("description", models.TextField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='PropertyOwner',
name="PropertyOwner",
fields=[
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
('phone_number', models.CharField(blank=True, max_length=20, null=True)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
(
"phone_number",
models.CharField(blank=True, max_length=20, null=True),
),
],
),
migrations.CreateModel(
name='Vendor',
name="Vendor",
fields=[
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
('business_name', models.CharField(max_length=100)),
('business_type', models.CharField(choices=[('contractor', 'Contractor'), ('inspector', 'Inspector'), ('lender', 'Lender'), ('other', 'Other')], max_length=20)),
('phone_number', models.CharField(blank=True, max_length=20, 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)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
("business_name", models.CharField(max_length=100)),
(
"business_type",
models.CharField(
choices=[
("contractor", "Contractor"),
("inspector", "Inspector"),
("lender", "Lender"),
("other", "Other"),
],
max_length=20,
),
),
(
"phone_number",
models.CharField(blank=True, max_length=20, 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)),
],
),
migrations.CreateModel(
name='Message',
name="Message",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('attachment', models.FileField(blank=True, null=True, upload_to=core.models.message_file_path)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('read', models.BooleanField(default=False)),
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='core.conversation')),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("text", models.TextField()),
(
"attachment",
models.FileField(
blank=True, null=True, upload_to=core.models.message_file_path
),
),
("timestamp", models.DateTimeField(auto_now_add=True)),
("read", models.BooleanField(default=False)),
(
"conversation",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="messages",
to="core.conversation",
),
),
(
"sender",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_messages",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name='PasswordResetToken',
name="PasswordResetToken",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddField(
model_name='conversation',
name='property',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.property'),
model_name="conversation",
name="property",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="conversations",
to="core.property",
),
),
migrations.CreateModel(
name='Video',
name="Video",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.TextField(blank=True, null=True)),
('link', models.URLField()),
('duration', models.IntegerField(help_text='Duration in seconds')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='videos', to='core.videocategory')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=200)),
("description", models.TextField(blank=True, null=True)),
("link", models.URLField()),
("duration", models.IntegerField(help_text="Duration in seconds")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="videos",
to="core.videocategory",
),
),
],
),
migrations.AddField(
model_name='property',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='core.propertyowner'),
model_name="property",
name="owner",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="properties",
to="core.propertyowner",
),
),
migrations.AddField(
model_name='conversation',
name='property_owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.propertyowner'),
model_name="conversation",
name="property_owner",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="conversations",
to="core.propertyowner",
),
),
migrations.AddField(
model_name='conversation',
name='vendor',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.vendor'),
model_name="conversation",
name="vendor",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="conversations",
to="core.vendor",
),
),
migrations.CreateModel(
name='UserVideoProgress',
name="UserVideoProgress",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('progress', models.IntegerField(default=0, help_text='Progress in seconds')),
('status', models.CharField(choices=[('not_started', 'Not Started'), ('in_progress', 'In Progress'), ('completed', 'Completed')], default='not_started', max_length=20)),
('last_watched', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='video_progress', to=settings.AUTH_USER_MODEL)),
('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_progress', to='core.video')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"progress",
models.IntegerField(default=0, help_text="Progress in seconds"),
),
(
"status",
models.CharField(
choices=[
("not_started", "Not Started"),
("in_progress", "In Progress"),
("completed", "Completed"),
],
default="not_started",
max_length=20,
),
),
("last_watched", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="video_progress",
to=settings.AUTH_USER_MODEL,
),
),
(
"video",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_progress",
to="core.video",
),
),
],
options={
'unique_together': {('user', 'video')},
"unique_together": {("user", "video")},
},
),
migrations.AlterUniqueTogether(
name='conversation',
unique_together={('property_owner', 'vendor', 'property')},
name="conversation",
unique_together={("property_owner", "vendor", "property")},
),
]

View File

@@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
("core", "0001_initial"),
]
operations = [
migrations.AlterUniqueTogether(
name='conversation',
name="conversation",
unique_together=set(),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.4 on 2025-07-15 18:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0002_alter_conversation_unique_together"),
]
operations = [
migrations.AlterField(
model_name="vendor",
name="business_type",
field=models.CharField(
choices=[
("electrician", "Electrician"),
("carpenter", "Carpenter"),
("plumber", "Plumber"),
("inspector", "Inspector"),
("lender", "Lender"),
("other", "Other"),
],
max_length=20,
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-07-16 01:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0003_alter_vendor_business_type"),
]
operations = [
migrations.AddField(
model_name="user",
name="tos_signed",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,70 @@
# Generated by Django 5.2.4 on 2025-07-17 13:27
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0004_user_tos_signed"),
]
operations = [
migrations.AddField(
model_name="user",
name="profile_created",
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name="Offer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"status",
models.CharField(
choices=[
("submitted", "Submitted"),
("draft", "Draft"),
("accepted", "Accepted"),
("rejected", "Rejected"),
("counter", "Counter"),
],
default="draft",
max_length=10,
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"previous_offer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.offer"
),
),
(
"proptery",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="core.property"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -0,0 +1,79 @@
# Generated by Django 5.2.4 on 2025-07-18 15:10
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0005_user_profile_created_offer"),
]
operations = [
migrations.AddField(
model_name="property",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="property",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="propertyowner",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="propertyowner",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="user",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="user",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="vendor",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="vendor",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="videocategory",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="videocategory",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-07-21 21:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0006_property_created_at_property_updated_at_and_more"),
]
operations = [
migrations.RenameField(
model_name="offer",
old_name="proptery",
new_name="property",
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.2.4 on 2025-07-21 21:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0007_rename_proptery_offer_property"),
]
operations = [
migrations.AlterField(
model_name="offer",
name="previous_offer",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="core.offer",
),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.2.4 on 2025-07-24 14:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0008_alter_offer_previous_offer"),
]
operations = [
migrations.AlterField(
model_name="offer",
name="status",
field=models.CharField(
choices=[
("submitted", "Submitted"),
("draft", "Draft"),
("accepted", "Accepted"),
("rejected", "Rejected"),
("counter", "Counter"),
("withdrawn", "Withdrawn"),
],
default="draft",
max_length=10,
),
),
migrations.AlterField(
model_name="video",
name="link",
field=models.FileField(upload_to="videos/"),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.4 on 2025-07-24 17:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0009_alter_offer_status_alter_video_link"),
]
operations = [
migrations.AlterField(
model_name="conversation",
name="property",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="conversations",
to="core.property",
),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.2.4 on 2025-07-24 18:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0010_alter_conversation_property"),
]
operations = [
migrations.AddField(
model_name="user",
name="tier",
field=models.CharField(
choices=[
("basic", "Basic"),
("premium", "Premium"),
("vendor", "Vendor"),
],
default="basic",
max_length=20,
),
),
]

View File

@@ -0,0 +1,126 @@
# Generated by Django 5.2.4 on 2025-08-01 09:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0011_user_tier"),
]
operations = [
migrations.AddField(
model_name="property",
name="description",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name="property",
name="features",
field=models.JSONField(blank=True, default=list),
),
migrations.AddField(
model_name="property",
name="latitude",
field=models.DecimalField(
blank=True, decimal_places=6, max_digits=9, null=True
),
),
migrations.AddField(
model_name="property",
name="longitude",
field=models.DecimalField(
blank=True, decimal_places=6, max_digits=9, null=True
),
),
migrations.AddField(
model_name="property",
name="num_bathrooms",
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="property",
name="num_bedrooms",
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="property",
name="realestate_api_id",
field=models.IntegerField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name="property",
name="sq_ft",
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="vendor",
name="average_rating",
field=models.DecimalField(
blank=True, decimal_places=2, max_digits=3, null=True
),
),
migrations.AddField(
model_name="vendor",
name="certifications",
field=models.JSONField(blank=True, default=list, null=True),
),
migrations.AddField(
model_name="vendor",
name="description",
field=models.CharField(default="", max_length=1024),
),
migrations.AddField(
model_name="vendor",
name="num_reviews",
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="vendor",
name="profile_picture",
field=models.URLField(blank=True, max_length=500, null=True),
),
migrations.AddField(
model_name="vendor",
name="service_areas",
field=models.JSONField(blank=True, default=list),
),
migrations.AddField(
model_name="vendor",
name="services",
field=models.JSONField(blank=True, default=list),
),
migrations.AddField(
model_name="vendor",
name="website",
field=models.URLField(blank=True, null=True),
),
migrations.CreateModel(
name="PropertyPictures",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("image", models.FileField(upload_to="pcitures/")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"Property",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="pictures",
to="core.property",
),
),
],
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-08-01 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0012_property_description_property_features_and_more"),
]
operations = [
migrations.AlterField(
model_name="property",
name="latitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AlterField(
model_name="property",
name="longitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
]

View File

@@ -0,0 +1,50 @@
# Generated by Django 5.2.4 on 2025-08-03 01:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0013_alter_property_latitude_alter_property_longitude"),
]
operations = [
migrations.AddField(
model_name="property",
name="listed_date",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="property",
name="listed_price",
field=models.DecimalField(
blank=True, decimal_places=2, max_digits=12, null=True
),
),
migrations.AddField(
model_name="property",
name="property_status",
field=models.CharField(
choices=[
("active", "Active"),
("pending", "Pending]"),
("contingent", "Contingent"),
("sold", "Sold"),
("off_market", "Off Market"),
],
default="off_market",
max_length=15,
),
),
migrations.AddField(
model_name="property",
name="saves",
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name="property",
name="views",
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,195 @@
# Generated by Django 5.2.4 on 2025-08-04 19:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0014_property_listed_date_property_listed_price_and_more"),
]
operations = [
migrations.CreateModel(
name="SchoolInfo",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("city", models.CharField(max_length=100)),
("state", models.CharField(max_length=2)),
("zip_code", models.CharField(max_length=10)),
(
"latitude",
models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
(
"longitude",
models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
("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(
choices=[("Public", "Public"), ("Other", "Other")],
default="public",
max_length=15,
),
),
],
),
migrations.AlterField(
model_name="property",
name="property_status",
field=models.CharField(
choices=[
("active", "Active"),
("pending", "Pending"),
("contingent", "Contingent"),
("sold", "Sold"),
("off_market", "Off Market"),
],
default="off_market",
max_length=15,
),
),
migrations.CreateModel(
name="OpenHouse",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("listed_date", models.DateTimeField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"property",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="open_houses",
to="core.property",
),
),
],
),
migrations.CreateModel(
name="PropertySaleInfo",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("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)),
(
"property",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sale_info",
to="core.property",
),
),
],
),
migrations.CreateModel(
name="PropertyTaxInfo",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("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)),
(
"property",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="tax_info",
to="core.property",
),
),
],
),
migrations.CreateModel(
name="PropertyWalkScoreInfo",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("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)),
(
"property",
models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="walk_score",
to="core.property",
),
),
],
),
migrations.AddField(
model_name="property",
name="schools",
field=models.ManyToManyField(to="core.schoolinfo"),
),
]

View File

@@ -0,0 +1,274 @@
# Generated by Django 5.2.4 on 2025-08-08 11:08
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0015_schoolinfo_alter_property_property_status_openhouse_and_more"),
]
operations = [
migrations.CreateModel(
name="Attorney",
fields=[
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
("firm_name", models.CharField(max_length=200)),
("bar_number", models.CharField(max_length=50, unique=True)),
(
"phone_number",
models.CharField(blank=True, max_length=20, 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)),
("years_experience", models.IntegerField(default=0)),
("website", models.URLField(blank=True, null=True)),
(
"profile_picture",
models.URLField(blank=True, max_length=500, null=True),
),
("bio", models.TextField(blank=True, null=True)),
("licensed_states", models.JSONField(blank=True, default=list)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"latitude",
models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
(
"longitude",
models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
],
),
migrations.CreateModel(
name="RealEstateAgent",
fields=[
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
("brokerage_name", models.CharField(max_length=200)),
("license_number", models.CharField(max_length=50, unique=True)),
(
"phone_number",
models.CharField(blank=True, max_length=20, 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)),
("years_experience", models.IntegerField(default=0)),
("website", models.URLField(blank=True, null=True)),
(
"profile_picture",
models.URLField(blank=True, max_length=500, null=True),
),
("bio", models.TextField(blank=True, null=True)),
("licensed_states", models.JSONField(blank=True, default=list)),
(
"agent_type",
models.CharField(
choices=[
("buyer_agent", "Buyer's Agent"),
("seller_agent", "Seller's Agent"),
("dual_agent", "Dual Agent"),
("other", "Other"),
],
default="other",
max_length=20,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"latitude",
models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
(
"longitude",
models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
],
),
migrations.AddField(
model_name="vendor",
name="latitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AddField(
model_name="vendor",
name="longitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AlterField(
model_name="user",
name="user_type",
field=models.CharField(
choices=[
("property_owner", "Property Owner"),
("vendor", "Vendor"),
("attorney", "Attorney"),
("real_estate_agent", "Real Estate Agent"),
("admin", "Admin"),
],
max_length=20,
),
),
migrations.CreateModel(
name="Bid",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.TextField()),
(
"bid_type",
models.CharField(
choices=[
("electrical", "Electrical"),
("plumbing", "Plumbing"),
("carpentry", "Carpentry"),
("general_contractor", "General Contractor"),
],
max_length=50,
),
),
(
"location",
models.CharField(
choices=[
("living_room", "Living Room"),
("basement", "Basement"),
("kitchen", "Kitchen"),
("bathroom", "Bathroom"),
("bedroom", "Bedroom"),
("outside", "Outside"),
],
max_length=50,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"property",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="bids",
to="core.property",
),
),
],
),
migrations.CreateModel(
name="BidImage",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("image", models.FileField(upload_to="bid_pictures/")),
("uploaded_at", models.DateTimeField(auto_now_add=True)),
(
"bid",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="images",
to="core.bid",
),
),
],
),
migrations.CreateModel(
name="BidResponse",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.TextField()),
("price", models.DecimalField(decimal_places=2, max_digits=10)),
(
"status",
models.CharField(
choices=[
("draft", "Draft"),
("submitted", "Submitted"),
("selected", "Selected"),
],
default="draft",
max_length=20,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"bid",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="responses",
to="core.bid",
),
),
(
"vendor",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="bid_responses",
to="core.vendor",
),
),
],
options={
"unique_together": {("bid", "vendor")},
},
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 5.2.4 on 2025-08-08 14:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0016_attorney_realestateagent_vendor_latitude_and_more"),
]
operations = [
migrations.RemoveField(
model_name="attorney",
name="latitude",
),
migrations.RemoveField(
model_name="attorney",
name="longitude",
),
migrations.RemoveField(
model_name="property",
name="schools",
),
migrations.RemoveField(
model_name="realestateagent",
name="latitude",
),
migrations.RemoveField(
model_name="realestateagent",
name="longitude",
),
migrations.RemoveField(
model_name="vendor",
name="latitude",
),
migrations.RemoveField(
model_name="vendor",
name="longitude",
),
migrations.AddField(
model_name="property",
name="street",
field=models.CharField(default="", max_length=200),
),
migrations.AddField(
model_name="schoolinfo",
name="properties",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="schools",
to="core.property",
),
),
]

View File

@@ -0,0 +1,55 @@
# Generated by Django 5.2.4 on 2025-08-08 16:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0017_remove_attorney_latitude_remove_attorney_longitude_and_more"),
]
operations = [
migrations.AddField(
model_name="attorney",
name="latitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AddField(
model_name="attorney",
name="longitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AddField(
model_name="realestateagent",
name="latitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AddField(
model_name="realestateagent",
name="longitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AddField(
model_name="vendor",
name="latitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
migrations.AddField(
model_name="vendor",
name="longitude",
field=models.DecimalField(
blank=True, decimal_places=27, max_digits=30, null=True
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-11 10:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0018_attorney_latitude_attorney_longitude_and_more"),
]
operations = [
migrations.AlterField(
model_name="attorney",
name="bar_number",
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-11 11:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0019_alter_attorney_bar_number"),
]
operations = [
migrations.AddField(
model_name="vendor",
name="views",
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 5.2.4 on 2025-08-11 18:27
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0020_vendor_views"),
]
operations = [
migrations.CreateModel(
name="UserViewModel",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-11 19:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0021_userviewmodel"),
]
operations = [
migrations.RenameField(
model_name="schoolinfo",
old_name="properties",
new_name="property",
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 5.2.4 on 2025-08-12 22:34
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0022_rename_properties_schoolinfo_property"),
]
operations = [
migrations.CreateModel(
name="PropertySave",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"property",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.property"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("user", "property")},
},
),
]

View File

@@ -1,5 +1,9 @@
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
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
@@ -8,33 +12,11 @@ from django.conf import settings
import uuid
import os
class TimeInfoBase(models.Model):
created = models.DateTimeField(default=timezone.now)
last_modified = models.DateTimeField(default=timezone.now)
class Meta:
abstract = True
def save(self, *args, **kwargs):
if not kwargs.pop("skip_last_modified", False) and not hasattr(
self, "skip_last_modified"
):
self.last_modified = timezone.now()
if kwargs.get("update_fields") is not None:
kwargs["update_fields"] = list(
{*kwargs["update_fields"], "last_modified"}
)
super().save(*args, **kwargs)
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('Users must have an email address')
raise ValueError("Users must have an email address")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
@@ -42,17 +24,25 @@ class UserManager(BaseUserManager):
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
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'),
('admin', 'Admin'),
("property_owner", "Property Owner"),
("vendor", "Vendor"),
("attorney", "Attorney"),
("real_estate_agent", "Real Estate 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)
@@ -60,36 +50,47 @@ class User(AbstractBaseUser, PermissionsMixin):
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']
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 = (
('contractor', 'Contractor'),
('inspector', 'Inspector'),
('lender', 'Lender'),
('other', 'Other'),
("electrician", "Electrician"),
("carpenter", "Carpenter"),
("plumber", "Plumber"),
("inspector", "Inspector"),
("lender", "Lender"),
("other", "Other"),
)
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
business_name = models.CharField(max_length=100)
business_type = models.CharField(max_length=20, choices=BUSINESS_TYPES)
@@ -98,121 +99,365 @@ class Vendor(models.Model):
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 Property(models.Model):
owner = models.ForeignKey(PropertyOwner, on_delete=models.CASCADE, related_name='properties')
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 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_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()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class PropertyPictures(models.Model):
Property = models.ForeignKey(
Property, on_delete=models.CASCADE, related_name="pictures"
)
image = models.FileField(upload_to="pcitures/")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
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')
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.URLField()
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'),
("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"
)
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')
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')
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'
self.status = "not_started"
elif self.progress >= self.video.duration:
self.status = 'completed'
self.status = "completed"
self.progress = self.video.duration
else:
self.status = 'in_progress'
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')
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]
ext = filename.split(".")[-1]
filename = f"{uuid.uuid4()}.{ext}"
return os.path.join('message_attachments', filename)
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')
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,
"user": self.user,
"reset_url": reset_url,
}
html_message = render_to_string('password_reset_email.html', context)
html_message = render_to_string("password_reset_email.html", context)
plain_message = strip_tags(html_message)
send_mail(
subject,
@@ -222,6 +467,90 @@ class PasswordResetToken(models.Model):
html_message=html_message,
fail_silently=False,
)
def __str__(self):
return f"Password reset token for {self.user.email}"
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')

View File

@@ -1,43 +1,63 @@
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
if hasattr(obj, 'owner'):
if hasattr(obj, "owner"):
return obj.owner.user == request.user
elif hasattr(obj, 'user'):
elif hasattr(obj, "user"):
return obj.user == request.user
return False
class IsPropertyOwner(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.user_type == 'property_owner'
return request.user.user_type == "property_owner"
def has_object_permission(self, request, view, obj):
if hasattr(obj, 'owner'):
if hasattr(obj, "owner"):
return obj.owner.user == request.user
elif hasattr(obj, 'property_owner'):
elif hasattr(obj, "property_owner"):
return obj.property_owner.user == request.user
return False
class IsVendor(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.user_type == 'vendor'
return request.user.user_type == "vendor"
def has_object_permission(self, request, view, obj):
if hasattr(obj, 'vendor'):
if hasattr(obj, "vendor"):
return obj.vendor.user == request.user
return False
class IsParticipant(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.user.user_type == 'property_owner':
owner = obj.property_owner if hasattr(obj, 'property_owner') else obj.conversation.property_owner
if request.user.user_type == "property_owner":
owner = (
obj.property_owner
if hasattr(obj, "property_owner")
else obj.conversation.property_owner
)
return owner.user == request.user
elif request.user.user_type == 'vendor':
vendor = obj.vendor if hasattr(obj, 'vendor') else obj.conversation.vendor
elif request.user.user_type == "vendor":
vendor = obj.vendor if hasattr(obj, "vendor") else obj.conversation.vendor
return vendor.user == request.user
return False
return False
class IsParticipantInOffer(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
"""
There are two options, either you are the sender or the owner of the property
"""
if hasattr(obj, "user") and hasattr(obj, "property"):
return request.user == obj.user or request.user == obj.property.owner.user
return False

View File

@@ -2,5 +2,6 @@ from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<conversation_id>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
re_path(r"ws/chat/(?P<account_id>\w+)/$", consumers.ChatConsumer.as_asgi()),
#re_path(r"ws/chat/$", consumers.ChatConsumer.as_asgi()),
]

View File

@@ -3,8 +3,23 @@ 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, Vendor, Property, VideoCategory, Video,
UserVideoProgress, Conversation, Message, PasswordResetToken
PropertyOwner,
Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
PasswordResetToken,
Offer,
PropertyPictures,
OpenHouse,
PropertySaleInfo,
PropertyTaxInfo,
PropertyWalkScoreInfo,
SchoolInfo,
Bid, BidImage, BidResponse, RealEstateAgent, Attorney, UserViewModel, PropertySave
)
from django.core.mail import send_mail
from django.template.loader import render_to_string
@@ -15,314 +30,813 @@ 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
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)
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,
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']
read_only_fields = ['id', 'is_active', 'date_joined']
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']
fields = [
"email",
"first_name",
"last_name",
"user_type",
"password",
"password2",
]
extra_kwargs = {
'password': {'write_only': True},
'password2': {'write_only': True},
"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."})
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')
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']
fields = ["user", "phone_number"]
read_only_fields = ["created_at", "updated_at"]
def create(self, validated_data):
user_data = validated_data.pop('user')
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)
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 = ['user', 'business_name', 'business_type', 'phone_number',
'address', 'city', 'state', 'zip_code']
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):
# Extract user data
user_data = validated_data.pop('user')
# Get or create category
user, _ = User.objects.get_or_create(**user_data)
# Create video with the category
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):
user_data = validated_data.pop('user', None)
# Update Vendor fields
# 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 nested User fields if provided
if user_data:
user = instance.user
email = user_data.get('email', None)
# Only validate email uniqueness if it's being changed
if email and email != user.email:
if User.objects.filter(email=email).exists():
raise serializers.ValidationError({
'email': 'A user with this email already exists.'
})
for attr, value in user_data.items():
setattr(user, attr, value)
user.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 PropertySerializer(serializers.ModelSerializer):
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"]
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 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 PropertyResponseSerializer(serializers.ModelSerializer):
owner = PropertyOwnerSerializer()
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', 'city', 'state', 'zip_code',
'market_value', 'loan_amount', 'loan_interest_rate',
'loan_term', 'loan_start_date']
read_only_fields = ['id']
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',
]
read_only_fields = ["id", "created_at", "updated_at"]
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
class VideoCategorySerializer(serializers.ModelSerializer):
class Meta:
model = VideoCategory
fields = ['id', 'name', 'description']
read_only_fields = ['id']
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']
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')
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)
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 = ['video', 'progress', 'status', 'last_watched', 'user']
read_only_fields = ['status', 'last_watched']
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')
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')
user = validated_data.pop("user")
# Create progress record
progress = UserVideoProgress.objects.create(
user=user,
video=video,
**validated_data
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)
video_data = validated_data.pop("video", None)
if video_data:
video_serializer = VideoSerializer(instance.video, data=video_data, partial=True)
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.progress = validated_data.get("progress", instance.progress)
instance.save()
return instance
class ConversationSerializer(serializers.ModelSerializer):
class Meta:
model = Conversation
fields = ['id', 'property_owner', 'vendor', 'property', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ['id', 'conversation', 'sender', 'text', 'attachment', 'timestamp', 'read']
read_only_fields = ['id', 'timestamp']
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')
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)
instance.text = validated_data.get("text", instance.text)
# Update read status if provided
if 'read' in validated_data:
instance.read = validated_data['read']
if "read" in validated_data:
instance.read = validated_data["read"]
# Handle attachment updates (if needed)
if 'attachment' in validated_data:
if "attachment" in validated_data:
# Delete old attachment if exists
if instance.attachment:
instance.attachment.delete()
instance.attachment = validated_data['attachment']
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'])
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 = 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'])
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."})
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'])
token = PasswordResetToken.objects.get(token=self.validated_data["token"])
user = token.user
user.set_password(self.validated_data['new_password'])
user.set_password(self.validated_data["new_password"])
user.save()
token.used = True
token.save()
return user
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 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

View File

View File

@@ -0,0 +1,18 @@
from abc import ABC, abstractmethod
from langchain_ollama import OllamaLLM
from langchain_core.output_parsers import StrOutputParser
class BaseService(ABC):
"""Abstract base class for LLM conversation services."""
def __init__(self, temperature=0.7):
self.llm = OllamaLLM(
model="llama3.2",
temperature=0.7,
top_k=50,
top_p=0.9,
repeat_penalty=1.1,
num_ctx=4096,
)
self.output_parser = StrOutputParser()

View File

@@ -0,0 +1,136 @@
from abc import ABC, abstractmethod
from typing import AsyncGenerator, Generator, Optional
# from langchain_community.llms import Ollama
from langchain_ollama import OllamaLLM
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from core.models import User
class LLMService(ABC):
"""sd;ofisdf"""
def __init__(self):
self.llm = OllamaLLM(
model="llama3.2",
temperature=0.7,
top_k=50,
top_p=0.9,
repeat_penalty=1.1,
num_ctx=4096,
)
self.output_parser = StrOutputParser()
@abstractmethod
def generate_response(self, query: str, **kwargs):
"""Generate a response to a query within a conversation context."""
pass
# def _format_history(self, conversation: Conversation) -> str:
# """Format conversation history for the prompt."""
# prompts = Prompt.objects.filter(conversation=conversation).order_by(
# "created_at"
# )
# return "\n".join(
# f"{'User' if prompt.is_user else 'AI'}: {prompt.text}" for prompt in prompts
# )
class AsyncLLMService(LLMService):
"""Asynchronous LLM conversation service."""
def __init__(self):
super().__init__()
self._setup_chain()
def _setup_chain(self):
"""Setup the conversation chain."""
template = """Role:
You are HomeSale Helper, an AI assistant designed to educate and guide residential property owners through the process of selling their homes. Your goal is to provide clear, trustworthy, and personalized advice on pricing, marketing, staging, negotiations, legal considerations, and closing processes.
Conversation: {conversation}
Knowledge Base:
Stay updated on real estate trends (without providing outdated data).
Understand local market variations (if location is provided).
Know best practices for home staging, photography, and listing optimization.
Explain legal/financial steps (disclosures, contracts, closing costs) in simple terms.
Tone & Style:
Friendly, professional, and empathetic (selling a home is emotional).
Avoid jargon; simplify complex topics.
Be neutral—never pressure users or favor specific agents/buyers.
Rules:
Never give financial/legal advice—direct users to consult professionals.
Never speculate on exact home values—instead, suggest comparative market analysis (CMA) methods.
Prioritize actionable steps (e.g., "Heres how to improve curb appeal: …").
If asked about trends, clarify whether data is general or location-specific.
Example Responses:
"To attract buyers, focus on decluttering and neutral paint colors. Would you like staging tips?"
"Closing costs typically range from 2%5% of the sale price. A real estate attorney can clarify specifics for your area."
"I cant calculate your homes exact value, but heres how to research local comps…"
"""
self.prompt = ChatPromptTemplate.from_template(template)
self.conversation_chain = (
{
# "context":lambda x: x["conversation"],
# "recent_history":lambda x: x['recent_conversation'],
"conversation": lambda x: x["conversation"],
}
| self.prompt
| self.llm
| self.output_parser
)
async def _format_history(self, conversation: list) -> str:
"""Async version of format conversation history."""
# prompts = list(
# await Prompt.objects.filter(conversation_id=conversation_id)
# .order_by("created")
# )
# return "\n".join(
# f"{'User' if prompt.is_user else 'AI'}: {prompt.text}" for prompt in prompts
# )
return "\n".join([f"{"User" if prompt.get('sender')=="user" else "AI"}: {prompt.get('text')}" for prompt in conversation])
# async def _get_recent_messages(self, conversation: list) -> str:
# """Async version of format conversation history."""
# # prompts = list(
# # await Prompt.objects.filter(conversation_id=conversation_id)
# # .order_by("created")
# # [-6:]
# # )
# # return "\n".join(
# # f"{'User' if prompt.is_user else 'AI'}: {prompt.text}" for prompt in prompts
# # )
# return "\n".join([f"{"User" if prompt.type=="human" else "AI"}: {prompt.text()}" for prompt in conversation])
async def generate_response(
self, conversation: list[dict[str,str]], user: User
) -> AsyncGenerator[str, None]:
"""Generate response with async streaming support."""
chain_input = {
"conversation": await self._format_history(conversation),
}
async for chunk in self.conversation_chain.astream(chain_input):
yield chunk

View File

@@ -0,0 +1,88 @@
from enum import Enum, auto
from typing import Dict, Any
from langchain_core.prompts import ChatPromptTemplate
from core.services.base_llm_service import BaseService
class ModerationLabel(Enum):
NSFW = auto()
FINE = auto()
class ModerationClassifier(BaseService):
"""
Classifies prompts as NSFW or FINE (safe) content.
"""
def __init__(self):
super().__init__(temperature=0.1)
# self.llm = OllamaLLM(
# model="llama3.2",
# temperature=0.1, # Very low for strict moderation
# top_k=10,
# num_ctx=2048,
# )
self.moderation_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""You are a strict content moderator. Classify the following prompt as either NSFW or FINE.
NSFW includes:
- Sexual content
- Violence/gore
- Hate speech
- Illegal activities
- Harassment
- Graphic/disturbing content
FINE includes:
- Safe for work topics
- General conversation
- Professional inquiries
- Creative requests (non-explicit)
- Technical questions
Examples:
- "How to make a bomb" → NSFW
- "Write a love poem" → FINE
- "Explicit sex scene" → NSFW
- "Python tutorial" → FINE
Return ONLY "NSFW" or "FINE", nothing else.""",
),
("human", "{prompt}"),
]
)
self.chain = self.moderation_prompt | self.llm
async def classify_async(self, prompt: str) -> ModerationLabel:
"""Asynchronous classification"""
try:
response = (await self.chain.ainvoke({"prompt": prompt})).strip().upper()
return self._parse_response(response)
except Exception as e:
print(f"Moderation error: {e}")
return ModerationLabel.NSFW # Fail-safe to NSFW
def classify(self, prompt: str) -> ModerationLabel:
"""Synchronous classification"""
try:
response = self.chain.invoke({"prompt": prompt}).strip().upper()
return self._parse_response(response)
except Exception as e:
print(f"Moderation error: {e}")
return ModerationLabel.NSFW # Fail-safe to NSFW
def _parse_response(self, response: str) -> ModerationLabel:
"""Convert string response to ModerationLabel enum"""
if "NSFW" in response:
return ModerationLabel.NSFW
return ModerationLabel.FINE # Default to FINE if unclear
# Singleton instance
moderation_classifier = ModerationClassifier()

View File

@@ -0,0 +1,102 @@
from langchain_ollama import OllamaLLM
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from .base_llm_service import BaseService
from core.models import Property
from typing import Generator
class PropertyDescriptionGenerator(BaseService):
def __init__(self):
super().__init__()
self._setup_chain()
# format_instructions = """
# Output should use ** for bold and acutal newlines.
# Example: '**Stunning** kitchen with \nprofessonal appliances'
# """
# self.output_parser = StrOutputParser().from_response_schemas([], format_instructions)
def _setup_chain(self):
template = """You are an expert real estate copywriter specializing in creating compelling residential property listings. Write a detailed, engaging description for the following property by incorporating its features, local context, and market appeal.
**Property Details:**
- Address: {address}
- City: {city}
- State: {state}
- ZIP: {zip_code}
- Market Value: ${market_value}
- Square Footage: {sq_ft}
- Bedrooms: {num_bedrooms}
- Bathrooms: {num_bathrooms}
- Features: {features_list}
- Coordinates: ({lat}, {lon})
**Instructions:**
1. First analyze the property's key selling points based on its features, size, and value proposition
2. Use the attached [property photos] to note any visible architectural styles, finishes, or unique elements
3. Make API calls (when available) to gather:
- Walkability score (0-100) from coordinates
- Nearby school ratings (GreatSchools or similar)
- Distance/time to major downtown areas
- Notable nearby amenities (parks, transit, shopping)
4. Craft the description with this structure:
- Engaging opening highlighting the most unique aspect
- Property details with flow that moves from exterior to interior
- Neighborhood context with hyper-local details
- Closing call-to-action emphasizing urgency or exclusivity
**Style Guidelines:**
- Use vivid but professional language (avoid clichés like 'dream home')
- Include specific local references (landmarks, neighborhood names)
- For pricing, use phrases like 'valued at' or 'priced to compete at'
- Mention 2-3 most impressive features first
- Keep between 150-250 words
- End with a 'Schedule your showing today!' variation
**API Tools Available (call if needed):**
- get_walkability_score(lat, lon)
- get_school_ratings(zip_code)
- get_nearby_amenities(lat, lon, radius=1mi)
- get_downtown_distance(lat, lon)
**Example Output Structure:**
'[Neighborhood/Architectural Hook]... This [bed/bath] [home_type] at [address] offers [key features]. Step inside to find [notable interior details]. The [specific room] features [detail]. Located just [time] from [landmark], enjoy [local perks]. [Call to action]'"""
self.prompt = ChatPromptTemplate.from_template(template)
self.conversation_chain = (
{
"address": lambda x: x["address"],
"city": lambda x: x["city"],
"state": lambda x: x["state"],
"zip_code": lambda x: x["zip_code"],
"market_value": lambda x: x["market_value"],
"sq_ft": lambda x: x["sq_ft"],
"num_bedrooms": lambda x: x["num_bedrooms"],
"num_bathrooms": lambda x: x["num_bathrooms"],
"features_list": lambda x: x["features_list"],
"lat": lambda x: x["lat"],
"lon": lambda x: x["lon"],
}
| self.prompt
| self.llm
| self.output_parser
)
def generate_response(
self, property: Property, **kwargs
) -> Generator[str, None, None]:
chain_input = {
"address": property.address,
"city": property.city,
"state": property.state,
"zip_code": property.zip_code,
"market_value": property.market_value,
"sq_ft": property.sq_ft,
"num_bedrooms": property.num_bedrooms,
"num_bathrooms": property.num_bathrooms,
"features_list": property.features,
"lat": property.latitude,
"lon": property.longitude,
}
return self.conversation_chain.invoke(chain_input)

View File

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

View File

@@ -0,0 +1,8 @@
from rest_framework.routers import DefaultRouter
from core.views import BidViewSet, BidResponseViewSet
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'bids', BidViewSet, basename='bids')
router.register(r'bid-responses', BidResponseViewSet, basename='bid-responses')
urlpatterns = router.urls

View File

@@ -3,19 +3,26 @@ from rest_framework.routers import DefaultRouter
from core.views import ConversationViewSet, MessageViewSet
router = DefaultRouter()
router.register(r'', ConversationViewSet, basename='conversation')
router.register(r"", ConversationViewSet, basename="conversation")
urlpatterns = [
path('<int:conversation_id>/messages/', MessageViewSet.as_view({
'get': 'list',
'post': 'create'
}), name='conversation-messages'),
path('<int:conversation_id>/messages/<int:pk>/', MessageViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}), name='message-detail'),
path(
"<int:conversation_id>/messages/",
MessageViewSet.as_view({"get": "list", "post": "create"}),
name="conversation-messages",
),
path(
"<int:conversation_id>/messages/<int:pk>/",
MessageViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="message-detail",
),
]
urlpatterns += router.urls
urlpatterns += router.urls

View File

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

View File

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

View File

@@ -3,6 +3,6 @@ from rest_framework.routers import DefaultRouter
from core.views import PropertyOwnerViewSet
router = DefaultRouter()
router.register(r'', PropertyOwnerViewSet, basename='property-owner')
router.register(r"", PropertyOwnerViewSet, basename="property-owner")
urlpatterns = router.urls
urlpatterns = router.urls

View File

@@ -0,0 +1,11 @@
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from core.views import PropertySaveViewSet
router = DefaultRouter()
router.register(r'', PropertySaveViewSet, basename='propertysave')
urlpatterns = [
path('', include(router.urls)),
]

View File

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

View File

@@ -3,6 +3,6 @@ from rest_framework.routers import DefaultRouter
from core.views import VendorViewSet
router = DefaultRouter()
router.register(r'', VendorViewSet, basename='vendor')
router.register(r"", VendorViewSet, basename="vendor")
urlpatterns = router.urls
urlpatterns = router.urls

View File

@@ -3,8 +3,9 @@ from rest_framework.routers import DefaultRouter
from core.views import VideoCategoryViewSet, VideoViewSet, UserVideoProgressViewSet
router = DefaultRouter()
router.register(r'categories', VideoCategoryViewSet, basename='video-category')
router.register(r'', VideoViewSet, basename='video')
router.register(r'progress', UserVideoProgressViewSet, basename='video-progress')
router.register(r"categories", VideoCategoryViewSet, basename="video-category")
router.register(r"progress", UserVideoProgressViewSet, basename="video-progress")
router.register(r"", VideoViewSet, basename="video")
urlpatterns = router.urls
urlpatterns = router.urls

View File

@@ -3,38 +3,107 @@ 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
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, Property, VideoCategory, Video,
UserVideoProgress, Conversation, Message
PropertyOwner,
Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
Offer,
PropertyWalkScoreInfo,
PropertyTaxInfo,
SchoolInfo,Bid, BidResponse, Attorney, RealEstateAgent, UserViewModel, PropertySave
)
from .serializers import (
CustomTokenObtainPairSerializer, UserSerializer, UserRegisterSerializer,
PropertyOwnerSerializer, VendorSerializer, PropertySerializer,
VideoCategorySerializer, VideoSerializer, UserVideoProgressSerializer,
ConversationSerializer, MessageSerializer, PasswordResetRequestSerializer,
PasswordResetConfirmSerializer
CustomTokenObtainPairSerializer,
UserSerializer,
UserRegisterSerializer,
PropertyOwnerSerializer,
VendorSerializer,
PropertyResponseSerializer,
PropertyRequestSerializer,
VideoCategorySerializer,
VideoSerializer,
UserVideoProgressSerializer,
ConversationRequestSerializer,
ConversationResponseSerializer,
MessageSerializer,
PasswordResetRequestSerializer,
PasswordResetConfirmSerializer,
OfferRequestSerializer,
OfferResponseSerializer,
PropertyPictureSerializer, BidSerializer, BidResponseSerializer, AttorneySerializer, RealEstateAgentSerializer, PropertySaveSerializer
)
from rest_framework.permissions import IsAuthenticated
from .permissions import IsOwnerOrReadOnly, IsPropertyOwner, IsVendor, IsParticipant
from .permissions import (
IsOwnerOrReadOnly,
IsPropertyOwner,
IsVendor,
IsParticipant,
IsParticipantInOffer,
)
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from django.db.models import Q
from .services.property_description_generator import PropertyDescriptionGenerator
from .filters import PropertyFilterSet
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]
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"]
@@ -44,130 +113,489 @@ class LogoutView(APIView):
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
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
status=status.HTTP_200_OK,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PropertyOwnerViewSet(viewsets.ModelViewSet):
queryset = PropertyOwner.objects.all()
serializer_class = PropertyOwnerSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
search_fields = ['user__first_name', 'user__last_name', 'user__email']
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):
queryset = Vendor.objects.all()
serializer_class = VendorSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
search_fields = ['business_name', 'user__first_name', 'user__last_name', 'user__email']
filterset_fields = ['business_type']
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)
class PropertyViewSet(viewsets.ModelViewSet):
serializer_class = PropertySerializer
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
search_fields = ['address', 'city', 'state', 'zip_code']
filterset_fields = ['owner', 'state', 'city']
def get_queryset(self):
user = self.request.user
if user.user_type == 'property_owner':
return Property.objects.filter(owner__user=user)
return Property.objects.all()
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):
if self.request.user.user_type == 'property_owner':
# 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 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)
serializer.save(owner=owner)
## 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']
search_fields = ["title", "description"]
filterset_fields = ["category"]
class UserVideoProgressViewSet(viewsets.ModelViewSet):
queryset = UserVideoProgress.objects.all()
serializer_class = UserVideoProgressSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
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):
serializer_class = ConversationSerializer
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':
if user.user_type == "property_owner":
owner = PropertyOwner.objects.get(user=user)
return Conversation.objects.filter(property_owner=owner)
elif user.user_type == 'vendor':
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':
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':
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_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')
return Message.objects.filter(conversation=conversation).order_by("timestamp")
def perform_create(self, serializer):
conversation_id = self.kwargs.get('conversation_id')
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
context["conversation_id"] = self.kwargs.get("conversation_id")
return context
class OfferViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsParticipantInOffer]
def get_serializer_class(self):
"""
Returns the serializer class to use depending on the action.
- For 'list' and 'retrieve' (read operations), use PropertyResponseSerializer.
- For 'create', 'update', 'partial_update' (write operations), use PropertyRequestSerializer.
"""
if self.action in ["list", "retrieve"]:
return OfferResponseSerializer
return OfferRequestSerializer
def get_queryset(self):
user_lookup = Q(user=self.request.user)
property_lookup = Q(property__owner__user=self.request.user)
null_previous_offer = Q(previous_offer=None)
return Offer.objects.filter(
(property_lookup & null_previous_offer)
| (user_lookup & null_previous_offer)
)
class BidViewSet(viewsets.ModelViewSet):
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.
"""
serializer.save(user=self.request.user)
def destroy(self, request, *args, **kwargs):
"""
Unsaves a property.
"""
try:
instance = self.get_object()
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
)

View File

@@ -8,17 +8,21 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import core.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
core.routing.websocket_urlpatterns
)
),
})
URLRouter(core.routing.websocket_urlpatterns)
),
}
)

View File

@@ -31,29 +31,41 @@ DEBUG = True
ALLOWED_HOSTS = ['*']
CORS_ALLOW_CREDENTIALS = False
CORS_ORIGIN_ALLOW_ALL = True
CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://127.0.0.1", "http://localhost:3000"]
ALLOWED_HOSTS = [
"localhost",
"127.0.0.1",
"localhost:3000",
"127.0.0.1:3000",
"10.0.0.9 njm:3000",
]
# Application definition
INSTALLED_APPS = [
"daphne",
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'channels',
'django_filters',
# Local apps
'core',
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -82,7 +94,7 @@ TEMPLATES = [
]
WSGI_APPLICATION = 'dta_service.wsgi.application'
ASGI_APPLICATION = 'config.asgi.application'
ASGI_APPLICATION = 'dta_service.asgi.application'
# Database
@@ -152,6 +164,10 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}
if DEBUG:
X_FRAME_OPTIONS = 'ALLOW-FROM 127.0.0.1:8010/'
# JWT Settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
@@ -168,8 +184,7 @@ SIMPLE_JWT = {
'JWK_URL': None,
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
"AUTH_HEADER_TYPES": ("JWT",),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
@@ -209,4 +224,8 @@ SIMPLE_JWT = {
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
TEST_DISCOVER_PATTERN = "test_*.py"
TEST_DISCOVER_PATTERN = "test_*.py"
# Realestate api
REAL_ESTATE_API_KEY = "AIMLOPERATIONSLLC-a7ce-7525-b38f-cf8b4d52ca70"

View File

@@ -20,22 +20,16 @@ from rest_framework_simplejwt.views import (
TokenRefreshView,
)
from core.views import (
CustomTokenObtainPairView, LogoutView,
CustomTokenObtainPairView, LogoutView,
PasswordResetRequestView, PasswordResetConfirmView,
UserRegisterView
UserRegisterView, UserRetrieveView, UserSignTosView, PropertyDescriptionView
)
from rest_framework.routers import DefaultRouter
from core.views import VideoCategoryViewSet, VideoViewSet, UserVideoProgressViewSet, VendorViewSet
router = DefaultRouter()
router.register(r'categories', VideoCategoryViewSet, basename='video-category')
router.register(r'', VideoViewSet, basename='video')
router.register(r'progress', UserVideoProgressViewSet, basename='video-progress')
router.register(r'', VendorViewSet, basename='vendor')
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
# Authentication
path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
@@ -43,11 +37,31 @@ urlpatterns = [
path('api/register/', UserRegisterView.as_view(), name='register'),
path('api/password-reset/', PasswordResetRequestView.as_view(), name='password_reset'),
path('api/password-reset/confirm/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
# API endpoints
path('api/user/', UserRetrieveView.as_view(), name='get_user'),
path('api/user/acknowledge_tos/', UserSignTosView.as_view(), name='sign_tos'),
path('api/property-description-generator/<int:property_id>/', PropertyDescriptionView.as_view(), name='property-description-generator'),
path('api/property-owners/', include('core.urls.property_owner')),
path('api/vendors/', include('core.urls.vendor')),
path('api/properties/', include('core.urls.property')),
path('api/videos/', include('core.urls.video')),
path('api/conversations/', include('core.urls.conversation')),
]
path('api/offers/', include('core.urls.offer')),
path('api/attorney/', include('core.urls.attorney')),
path('api/real_estate_agent/', include('core.urls.real_estate_agent')),
path('api/saved-properties/', include('core.urls.property_save')),
path('api/', include('core.urls.bid')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
def print_patterns(patterns, base_path=""):
for pattern in patterns:
if hasattr(pattern, 'url_patterns'): # It's a URLResolver
print_patterns(pattern.url_patterns, base_path + str(pattern.pattern))
else: # It's a URLPattern
full_path = base_path + str(pattern.pattern)
view_name = pattern.callback.__module__ + "." + pattern.callback.__name__
print(f"URL: /{full_path.replace('^', '').replace('$', '')} -> View: {view_name}")
print_patterns(urlpatterns)

View File

@@ -1,4 +1,4 @@
# core/tests/__init__.py
from .test_models import *
from .test_serializers import *
from .test_views import *
from .test_views import *

View File

@@ -1,328 +1,337 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from core.models import (
PropertyOwner, Vendor, Property, VideoCategory, Video,
UserVideoProgress, Conversation, Message, PasswordResetToken
PropertyOwner,
Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
PasswordResetToken,
)
from datetime import datetime, timedelta
from django.utils import timezone
User = get_user_model()
class UserModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='test@example.com',
first_name='Test',
last_name='User',
user_type='property_owner',
password='testpass123'
email="test@example.com",
first_name="Test",
last_name="User",
user_type="property_owner",
password="testpass123",
)
def test_user_creation(self):
self.assertEqual(self.user.email, 'test@example.com')
self.assertEqual(self.user.first_name, 'Test')
self.assertEqual(self.user.last_name, 'User')
self.assertEqual(self.user.user_type, 'property_owner')
self.assertTrue(self.user.check_password('testpass123'))
self.assertEqual(self.user.email, "test@example.com")
self.assertEqual(self.user.first_name, "Test")
self.assertEqual(self.user.last_name, "User")
self.assertEqual(self.user.user_type, "property_owner")
self.assertTrue(self.user.check_password("testpass123"))
def test_user_str(self):
self.assertEqual(str(self.user), 'test@example.com')
self.assertEqual(str(self.user), "test@example.com")
class PropertyOwnerModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='owner@example.com',
first_name='Property',
last_name='Owner',
user_type='property_owner',
password='testpass123'
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(
user=self.user,
phone_number='1234567890'
user=self.user, phone_number="1234567890"
)
def test_property_owner_creation(self):
self.assertEqual(self.owner.user, self.user)
self.assertEqual(self.owner.phone_number, '1234567890')
self.assertEqual(self.owner.phone_number, "1234567890")
def test_property_owner_str(self):
self.assertEqual(str(self.owner), 'Property Owner')
self.assertEqual(str(self.owner), "Property Owner")
class VendorModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='vendor@example.com',
first_name='Vendor',
last_name='User',
user_type='vendor',
password='testpass123'
email="vendor@example.com",
first_name="Vendor",
last_name="User",
user_type="vendor",
password="testpass123",
)
self.vendor = Vendor.objects.create(
user=self.user,
business_name='Test Vendor',
business_type='contractor',
phone_number='1234567890',
address='123 Test St',
city='Testville',
state='TS',
zip_code='12345'
business_name="Test Vendor",
business_type="contractor",
phone_number="1234567890",
address="123 Test St",
city="Testville",
state="TS",
zip_code="12345",
)
def test_vendor_creation(self):
self.assertEqual(self.vendor.user, self.user)
self.assertEqual(self.vendor.business_name, 'Test Vendor')
self.assertEqual(self.vendor.business_type, 'contractor')
self.assertEqual(self.vendor.phone_number, '1234567890')
self.assertEqual(self.vendor.address, '123 Test St')
self.assertEqual(self.vendor.city, 'Testville')
self.assertEqual(self.vendor.state, 'TS')
self.assertEqual(self.vendor.zip_code, '12345')
self.assertEqual(self.vendor.business_name, "Test Vendor")
self.assertEqual(self.vendor.business_type, "contractor")
self.assertEqual(self.vendor.phone_number, "1234567890")
self.assertEqual(self.vendor.address, "123 Test St")
self.assertEqual(self.vendor.city, "Testville")
self.assertEqual(self.vendor.state, "TS")
self.assertEqual(self.vendor.zip_code, "12345")
def test_vendor_str(self):
self.assertEqual(str(self.vendor), 'Test Vendor')
self.assertEqual(str(self.vendor), "Test Vendor")
class PropertyModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='owner@example.com',
first_name='Property',
last_name='Owner',
user_type='property_owner',
password='testpass123'
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(user=self.user)
self.property = Property.objects.create(
owner=self.owner,
address='123 Main St',
city='Anytown',
state='CA',
zip_code='90210',
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
loan_amount=400000.00,
loan_interest_rate=3.5,
loan_term=30,
loan_start_date='2020-01-01'
loan_start_date="2020-01-01",
)
def test_property_creation(self):
self.assertEqual(self.property.owner, self.owner)
self.assertEqual(self.property.address, '123 Main St')
self.assertEqual(self.property.city, 'Anytown')
self.assertEqual(self.property.state, 'CA')
self.assertEqual(self.property.zip_code, '90210')
self.assertEqual(self.property.address, "123 Main St")
self.assertEqual(self.property.city, "Anytown")
self.assertEqual(self.property.state, "CA")
self.assertEqual(self.property.zip_code, "90210")
self.assertEqual(self.property.market_value, 500000.00)
self.assertEqual(self.property.loan_amount, 400000.00)
self.assertEqual(self.property.loan_interest_rate, 3.5)
self.assertEqual(self.property.loan_term, 30)
self.assertEqual(str(self.property.loan_start_date), '2020-01-01')
self.assertEqual(str(self.property.loan_start_date), "2020-01-01")
def test_property_str(self):
self.assertEqual(str(self.property), '123 Main St, Anytown, CA 90210')
self.assertEqual(str(self.property), "123 Main St, Anytown, CA 90210")
class VideoModelTest(TestCase):
def setUp(self):
self.category = VideoCategory.objects.create(
name='Test Category',
description='Test Description'
name="Test Category", description="Test Description"
)
self.video = Video.objects.create(
category=self.category,
title='Test Video',
description='Test Video Description',
link='https://example.com/video',
duration=300
title="Test Video",
description="Test Video Description",
link="https://example.com/video",
duration=300,
)
def test_video_creation(self):
self.assertEqual(self.video.category, self.category)
self.assertEqual(self.video.title, 'Test Video')
self.assertEqual(self.video.description, 'Test Video Description')
self.assertEqual(self.video.link, 'https://example.com/video')
self.assertEqual(self.video.title, "Test Video")
self.assertEqual(self.video.description, "Test Video Description")
self.assertEqual(self.video.link, "https://example.com/video")
self.assertEqual(self.video.duration, 300)
def test_video_str(self):
self.assertEqual(str(self.video), 'Test Video')
self.assertEqual(str(self.video), "Test Video")
class UserVideoProgressModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='user@example.com',
first_name='Test',
last_name='User',
user_type='property_owner',
password='testpass123'
email="user@example.com",
first_name="Test",
last_name="User",
user_type="property_owner",
password="testpass123",
)
self.category = VideoCategory.objects.create(name='Test Category')
self.category = VideoCategory.objects.create(name="Test Category")
self.video = Video.objects.create(
category=self.category,
title='Test Video',
link='https://example.com/video',
duration=300
title="Test Video",
link="https://example.com/video",
duration=300,
)
self.progress = UserVideoProgress.objects.create(
user=self.user,
video=self.video,
progress=150
user=self.user, video=self.video, progress=150
)
def test_progress_creation(self):
self.assertEqual(self.progress.user, self.user)
self.assertEqual(self.progress.video, self.video)
self.assertEqual(self.progress.progress, 150)
self.assertEqual(self.progress.status, 'in_progress')
self.assertEqual(self.progress.status, "in_progress")
def test_status_update(self):
# Test not started
self.progress.progress = 0
self.progress.save()
self.assertEqual(self.progress.status, 'not_started')
self.assertEqual(self.progress.status, "not_started")
# Test completed
self.progress.progress = 300
self.progress.save()
self.assertEqual(self.progress.status, 'completed')
self.assertEqual(self.progress.status, "completed")
# Test in progress
self.progress.progress = 150
self.progress.save()
self.assertEqual(self.progress.status, 'in_progress')
self.assertEqual(self.progress.status, "in_progress")
def test_progress_str(self):
self.assertEqual(str(self.progress), 'user@example.com - Test Video - in_progress')
self.assertEqual(
str(self.progress), "user@example.com - Test Video - in_progress"
)
class ConversationModelTest(TestCase):
def setUp(self):
self.owner_user = User.objects.create_user(
email='owner@example.com',
first_name='Property',
last_name='Owner',
user_type='property_owner',
password='testpass123'
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user(
email='vendor@example.com',
first_name='Vendor',
last_name='User',
user_type='vendor',
password='testpass123'
email="vendor@example.com",
first_name="Vendor",
last_name="User",
user_type="vendor",
password="testpass123",
)
self.vendor = Vendor.objects.create(
user=self.vendor_user,
business_name='Test Vendor',
business_type='contractor'
business_name="Test Vendor",
business_type="contractor",
)
self.property = Property.objects.create(
owner=self.owner,
address='123 Main St',
city='Anytown',
state='CA',
zip_code='90210',
market_value=500000.00
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
)
self.conversation = Conversation.objects.create(
property_owner=self.owner,
vendor=self.vendor,
property=self.property
property_owner=self.owner, vendor=self.vendor, property=self.property
)
def test_conversation_creation(self):
self.assertEqual(self.conversation.property_owner, self.owner)
self.assertEqual(self.conversation.vendor, self.vendor)
self.assertEqual(self.conversation.property, self.property)
def test_conversation_str(self):
expected_str = f"Conversation between {self.owner} and {self.vendor} about {self.property}"
expected_str = (
f"Conversation between {self.owner} and {self.vendor} about {self.property}"
)
self.assertEqual(str(self.conversation), expected_str)
class MessageModelTest(TestCase):
def setUp(self):
self.owner_user = User.objects.create_user(
email='owner@example.com',
first_name='Property',
last_name='Owner',
user_type='property_owner',
password='testpass123'
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user(
email='vendor@example.com',
first_name='Vendor',
last_name='User',
user_type='vendor',
password='testpass123'
email="vendor@example.com",
first_name="Vendor",
last_name="User",
user_type="vendor",
password="testpass123",
)
self.vendor = Vendor.objects.create(
user=self.vendor_user,
business_name='Test Vendor',
business_type='contractor'
business_name="Test Vendor",
business_type="contractor",
)
self.property = Property.objects.create(
owner=self.owner,
address='123 Main St',
city='Anytown',
state='CA',
zip_code='90210',
market_value=500000.00
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
)
self.conversation = Conversation.objects.create(
property_owner=self.owner,
vendor=self.vendor,
property=self.property
property_owner=self.owner, vendor=self.vendor, property=self.property
)
self.message = Message.objects.create(
conversation=self.conversation,
sender=self.owner_user,
text='Test message'
conversation=self.conversation, sender=self.owner_user, text="Test message"
)
def test_message_creation(self):
self.assertEqual(self.message.conversation, self.conversation)
self.assertEqual(self.message.sender, self.owner_user)
self.assertEqual(self.message.text, 'Test message')
self.assertEqual(self.message.text, "Test message")
self.assertFalse(self.message.read)
def test_message_str(self):
expected_str = f"Message from {self.owner_user} in {self.conversation}"
self.assertEqual(str(self.message), expected_str)
class PasswordResetTokenModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='user@example.com',
first_name='Test',
last_name='User',
user_type='property_owner',
password='testpass123'
email="user@example.com",
first_name="Test",
last_name="User",
user_type="property_owner",
password="testpass123",
)
self.token = PasswordResetToken.objects.create(
user=self.user,
expires_at=timezone.now() + timedelta(hours=24)
user=self.user, expires_at=timezone.now() + timedelta(hours=24)
)
def test_token_creation(self):
self.assertEqual(self.token.user, self.user)
self.assertFalse(self.token.used)
self.assertTrue(self.token.is_valid())
def test_token_invalid_after_use(self):
self.token.used = True
self.token.save()
self.assertFalse(self.token.is_valid())
def test_token_invalid_after_expiry(self):
self.token.expires_at = timezone.now() - timedelta(hours=1)
self.token.save()
self.assertFalse(self.token.is_valid())
def test_token_str(self):
self.assertEqual(str(self.token), f"Password reset token for {self.user.email}")
self.assertEqual(str(self.token), f"Password reset token for {self.user.email}")

View File

@@ -2,510 +2,710 @@ from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.exceptions import ValidationError
from core.serializers import (
UserRegisterSerializer, PropertyOwnerSerializer, VendorSerializer,
PropertySerializer, VideoSerializer, UserVideoProgressSerializer,
ConversationSerializer, MessageSerializer, PasswordResetRequestSerializer,
PasswordResetConfirmSerializer
UserRegisterSerializer,
PropertyOwnerSerializer,
VendorSerializer,
PropertyRequestSerializer,
VideoSerializer,
UserVideoProgressSerializer,
ConversationRequestSerializer,
ConversationResponseSerializer,
MessageSerializer,
PasswordResetRequestSerializer,
PasswordResetConfirmSerializer,
OfferResponseSerializer,
OfferRequestSerializer,
)
from core.models import (
PropertyOwner, Vendor, Property, VideoCategory, Video,
UserVideoProgress, Conversation, Message, PasswordResetToken
PropertyOwner,
Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
PasswordResetToken,
Offer,
)
import uuid
from datetime import datetime, timedelta
User = get_user_model()
class UserRegisterSerializerTest(TestCase):
def setUp(self):
self.valid_data = {
'email': 'test@example.com',
'first_name': 'Test',
'last_name': 'User',
'user_type': 'property_owner',
'password': 'testpass123',
'password2': 'testpass123'
"email": "test@example.com",
"first_name": "Test",
"last_name": "User",
"user_type": "property_owner",
"password": "testpass123",
"password2": "testpass123",
}
def test_valid_serializer(self):
serializer = UserRegisterSerializer(data=self.valid_data)
self.assertTrue(serializer.is_valid())
user = serializer.save()
self.assertEqual(user.email, 'test@example.com')
self.assertEqual(user.first_name, 'Test')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.user_type, 'property_owner')
self.assertEqual(user.email, "test@example.com")
self.assertEqual(user.first_name, "Test")
self.assertEqual(user.last_name, "User")
self.assertEqual(user.user_type, "property_owner")
def test_password_mismatch(self):
invalid_data = self.valid_data.copy()
invalid_data['password2'] = 'differentpass'
invalid_data["password2"] = "differentpass"
serializer = UserRegisterSerializer(data=invalid_data)
with self.assertRaises(ValidationError):
serializer.is_valid(raise_exception=True)
class PropertyOwnerSerializerTest(TestCase):
def setUp(self):
self.user_data = {
'email': 'owner@example.com',
'first_name': 'Property',
'last_name': 'Owner',
'user_type': 'property_owner',
'password': 'testpass123'
"email": "owner@example.com",
"first_name": "Property",
"last_name": "Owner",
"user_type": "property_owner",
"password": "testpass123",
}
self.owner_data = {
'user': self.user_data,
'phone_number': '1234567890'
}
self.owner_data = {"user": self.user_data, "phone_number": "1234567890"}
def test_create_property_owner(self):
serializer = PropertyOwnerSerializer(data=self.owner_data)
self.assertTrue(serializer.is_valid())
owner = serializer.save()
self.assertEqual(owner.user.email, 'owner@example.com')
self.assertEqual(owner.phone_number, '1234567890')
self.assertEqual(owner.user.email, "owner@example.com")
self.assertEqual(owner.phone_number, "1234567890")
def test_update_property_owner(self):
user = User.objects.create_user(**self.user_data)
owner = PropertyOwner.objects.create(user=user, phone_number='1234567890')
owner = PropertyOwner.objects.create(user=user, phone_number="1234567890")
update_data = {
'user': {
'first_name': 'NewName',
'last_name': 'NewLast',
'email': 'newowner@example.com'
"user": {
"first_name": "NewName",
"last_name": "NewLast",
"email": "newowner@example.com",
},
'phone_number': '9876543210'
"phone_number": "9876543210",
}
serializer = PropertyOwnerSerializer(instance=owner, data=update_data, partial=True)
serializer = PropertyOwnerSerializer(
instance=owner, data=update_data, partial=True
)
self.assertTrue(serializer.is_valid())
updated_owner = serializer.save()
self.assertEqual(updated_owner.user.first_name, 'NewName')
self.assertEqual(updated_owner.user.last_name, 'NewLast')
self.assertEqual(updated_owner.phone_number, '9876543210')
self.assertEqual(updated_owner.user.first_name, "NewName")
self.assertEqual(updated_owner.user.last_name, "NewLast")
self.assertEqual(updated_owner.phone_number, "9876543210")
class VendorSerializerTest(TestCase):
def setUp(self):
self.user_data = {
'email': 'vendor@example.com',
'first_name': 'Vendor',
'last_name': 'User',
'user_type': 'vendor',
'password': 'testpass123'
"email": "vendor@example.com",
"first_name": "Vendor",
"last_name": "User",
"user_type": "vendor",
"password": "testpass123",
}
self.vendor_data = {
'user': self.user_data,
'business_name': 'Test Vendor',
'business_type': 'contractor',
'phone_number': '1234567890',
'address': '123 Test St',
'city': 'Testville',
'state': 'TS',
'zip_code': '12345'
"user": self.user_data,
"business_name": "Test Vendor",
"business_type": "electrician",
"phone_number": "1234567890",
"address": "123 Test St",
"city": "Testville",
"state": "TS",
"zip_code": "12345",
}
def test_create_vendor(self):
serializer = VendorSerializer(data=self.vendor_data)
self.assertTrue(serializer.is_valid())
self.assertTrue(serializer.is_valid(), serializer.errors)
vendor = serializer.save()
self.assertEqual(vendor.user.email, 'vendor@example.com')
self.assertEqual(vendor.business_name, 'Test Vendor')
self.assertEqual(vendor.business_type, 'contractor')
self.assertEqual(vendor.phone_number, '1234567890')
self.assertEqual(vendor.address, '123 Test St')
self.assertEqual(vendor.city, 'Testville')
self.assertEqual(vendor.state, 'TS')
self.assertEqual(vendor.zip_code, '12345')
self.assertEqual(vendor.user.email, "vendor@example.com")
self.assertEqual(vendor.business_name, "Test Vendor")
self.assertEqual(vendor.business_type, "electrician")
self.assertEqual(vendor.phone_number, "1234567890")
self.assertEqual(vendor.address, "123 Test St")
self.assertEqual(vendor.city, "Testville")
self.assertEqual(vendor.state, "TS")
self.assertEqual(vendor.zip_code, "12345")
def test_update_vendor(self):
user = User.objects.create_user(**self.user_data)
vendor = Vendor.objects.create(
user=user,
business_name='Test Vendor',
business_type='contractor',
phone_number='1234567890',
address='123 Test St',
city='Testville',
state='TS',
zip_code='12345'
business_name="Test Vendor",
business_type="electrician",
phone_number="1234567890",
address="123 Test St",
city="Testville",
state="TS",
zip_code="12345",
)
update_data = {
'user': {
'first_name': 'NewVendor',
'last_name': 'NewUser',
'email': 'newvendor@example.com'
"user": {
"first_name": "NewVendor",
"last_name": "NewUser",
"email": "newvendor@example.com",
},
'business_name': 'Updated Vendor',
'phone_number': '9876543210'
"business_name": "Updated Vendor",
"phone_number": "9876543210",
}
serializer = VendorSerializer(instance=vendor, data=update_data, partial=True)
self.assertTrue(serializer.is_valid())
updated_vendor = serializer.save()
self.assertEqual(updated_vendor.user.first_name, 'NewVendor')
self.assertEqual(updated_vendor.user.last_name, 'NewUser')
self.assertEqual(updated_vendor.business_name, 'Updated Vendor')
self.assertEqual(updated_vendor.phone_number, '9876543210')
self.assertEqual(updated_vendor.user.first_name, "NewVendor")
self.assertEqual(updated_vendor.user.last_name, "NewUser")
self.assertEqual(updated_vendor.business_name, "Updated Vendor")
self.assertEqual(updated_vendor.phone_number, "9876543210")
class PropertySerializerTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='owner@example.com',
first_name='Property',
last_name='Owner',
user_type='property_owner',
password='testpass123'
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(user=self.user)
self.property_data = {
'owner': self.owner.pk,
'address': '123 Main St',
'city': 'Anytown',
'state': 'CA',
'zip_code': '90210',
'market_value': '500000.00',
'loan_amount': '400000.00',
'loan_interest_rate': '3.50',
'loan_term': 30,
'loan_start_date': '2020-01-01'
"owner": self.owner.pk,
"address": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip_code": "90210",
"market_value": "500000.00",
"loan_amount": "400000.00",
"loan_interest_rate": "3.50",
"loan_term": 30,
"loan_start_date": "2020-01-01",
}
self.full_property_data = {
"owner": self.owner.pk,
"address": "1968 Green Apple Ct, Orange Park, FL, 32073",
"city": "Wheaton",
"state": "IL",
"zip_code": "60189",
"latitude": 41.833230929728565,
"longitude": -88.12083257242568,
"market_value": "742000",
"loan_amount": "449000",
"loan_term": "360",
"loan_start_date": "2021-01-05",
"description": "",
"features": [],
"pictures": [],
"num_bedrooms": None,
"num_bathrooms": 3,
"sq_ft": 2598,
"realestate_api_id": 175468968,
"views": 0,
"saves": 0,
"property_status": "off_market",
"schools": [
{
"city": "Wheaton",
"state": "IL",
"zip_code": "60189",
"latitude": 41.834869,
"longitude": -88.146118,
"school_type": "Public",
"enrollment": 1982,
"grades": "9-12",
"name": "Wheaton Warrenville South High School",
"parent_rating": 3,
"rating": 8,
},
{
"city": "Wheaton",
"state": "IL",
"zip_code": "60189",
"latitude": 41.852871,
"longitude": -88.109077,
"school_type": "Public",
"enrollment": 620,
"grades": "6-8",
"name": "Edison Middle School",
"parent_rating": 4,
"rating": 4,
},
{
"city": "Wheaton",
"state": "IL",
"zip_code": "60189",
"latitude": 41.851345,
"longitude": -88.129822,
"school_type": "Public",
"enrollment": 507,
"grades": "PK-5",
"name": "Madison Elementary School",
"parent_rating": 4,
"rating": 4,
},
{
"city": "Wheaton",
"state": "IL",
"zip_code": "60189",
"latitude": 41.857048,
"longitude": -88.108467,
"school_type": "Public",
"enrollment": 459,
"grades": "K-5",
"name": "Whittier Elementary School",
"parent_rating": 5,
"rating": 4,
},
],
"tax_info": {
"assessed_value": 202255,
"assessment_year": 2024,
"tax_amount": 12520.12,
"year": 2024,
},
"sale_info": [
{
"seq_no": 1,
"sale_date": "2025-04-22T00:00:00.000Z",
"sale_amount": 0,
},
{
"seq_no": 2,
"sale_date": "2020-06-26T00:00:00.000Z",
"sale_amount": 475000,
},
],
"owner": 5,
"created_at": "2025-08-04",
"last_updated": "2025-08-04",
"open_houses": [],
}
def test_create_property(self):
serializer = PropertySerializer(data=self.property_data)
self.assertTrue(serializer.is_valid())
serializer = PropertyRequestSerializer(data=self.property_data)
self.assertTrue(serializer.is_valid(), serializer.errors)
property = serializer.save()
self.assertEqual(property.owner, self.owner)
self.assertEqual(property.address, '123 Main St')
self.assertEqual(property.city, 'Anytown')
self.assertEqual(property.state, 'CA')
self.assertEqual(property.zip_code, '90210')
self.assertEqual(str(property.market_value), '500000.00')
self.assertEqual(str(property.loan_amount), '400000.00')
self.assertEqual(str(property.loan_interest_rate), '3.50')
self.assertEqual(property.address, "123 Main St")
self.assertEqual(property.city, "Anytown")
self.assertEqual(property.state, "CA")
self.assertEqual(property.zip_code, "90210")
self.assertEqual(str(property.market_value), "500000.00")
self.assertEqual(str(property.loan_amount), "400000.00")
self.assertEqual(str(property.loan_interest_rate), "3.50")
self.assertEqual(property.loan_term, 30)
self.assertEqual(str(property.loan_start_date), '2020-01-01')
self.assertEqual(str(property.loan_start_date), "2020-01-01")
def test_create_full_property(self):
serializer = PropertyRequestSerializer(data=self.full_property_data)
self.assertTrue(serializer.is_valid(), serializer.errors)
property = serializer.save()
def test_update_property(self):
property = Property.objects.create(
owner=self.owner,
address='123 Main St',
city='Anytown',
state='CA',
zip_code='90210',
market_value=500000.00
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
)
update_data = {
'address': '456 New St',
'city': 'Newtown',
'state': 'NY',
'zip_code': '10001',
'market_value': '600000.00'
"address": "456 New St",
"city": "Newtown",
"state": "NY",
"zip_code": "10001",
"market_value": "600000.00",
}
serializer = PropertySerializer(instance=property, data=update_data, partial=True)
serializer = PropertyRequestSerializer(
instance=property, data=update_data, partial=True
)
self.assertTrue(serializer.is_valid())
updated_property = serializer.save()
self.assertEqual(updated_property.address, '456 New St')
self.assertEqual(updated_property.city, 'Newtown')
self.assertEqual(updated_property.state, 'NY')
self.assertEqual(updated_property.zip_code, '10001')
self.assertEqual(str(updated_property.market_value), '600000.00')
self.assertEqual(updated_property.address, "456 New St")
self.assertEqual(updated_property.city, "Newtown")
self.assertEqual(updated_property.state, "NY")
self.assertEqual(updated_property.zip_code, "10001")
self.assertEqual(str(updated_property.market_value), "600000.00")
class VideoSerializerTest(TestCase):
def setUp(self):
self.category = VideoCategory.objects.create(
name='Test Category',
description='Test Description'
name="Test Category", description="Test Description"
)
self.video_data = {
'category': {
'id': self.category.id,
'name': self.category.name,
'description': self.category.description
"category": {
"id": self.category.id,
"name": self.category.name,
"description": self.category.description,
},
'title': 'Test Video',
'description': 'Test Video Description',
'link': 'https://example.com/video',
'duration': 300
"title": "Test Video",
"description": "Test Video Description",
"link": "https://example.com/video",
"duration": 300,
}
def test_video_serializer(self):
serializer = VideoSerializer(data=self.video_data)
self.assertTrue(serializer.is_valid())
video = serializer.save()
self.assertEqual(video.category, self.category)
self.assertEqual(video.title, 'Test Video')
self.assertEqual(video.description, 'Test Video Description')
self.assertEqual(video.link, 'https://example.com/video')
self.assertEqual(video.title, "Test Video")
self.assertEqual(video.description, "Test Video Description")
self.assertEqual(video.link, "https://example.com/video")
self.assertEqual(video.duration, 300)
class UserVideoProgressSerializerTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='user@example.com',
first_name='Test',
last_name='User',
user_type='property_owner',
password='testpass123'
email="user@example.com",
first_name="Test",
last_name="User",
user_type="property_owner",
password="testpass123",
)
self.category = VideoCategory.objects.create(name='Test Category')
self.category = VideoCategory.objects.create(name="Test Category")
self.video = Video.objects.create(
category=self.category,
title='Test Video',
link='https://example.com/video',
duration=300
title="Test Video",
link="https://example.com/video",
duration=300,
)
self.progress_data = {
'video': {
'id': self.video.id,
'title': self.video.title,
'link': self.video.link,
'duration': self.video.duration,
'category': {
'id': self.category.id,
'name': self.category.name
}
"video": {
"id": self.video.id,
"title": self.video.title,
"link": self.video.link,
"duration": self.video.duration,
"category": {"id": self.category.id, "name": self.category.name},
},
'user': self.user.pk,
'progress': 150
"user": self.user.pk,
"progress": 150,
}
def test_progress_serializer(self):
serializer = UserVideoProgressSerializer(data=self.progress_data)
self.assertTrue(serializer.is_valid())
progress = serializer.save(user=self.user)
self.assertEqual(progress.user, self.user)
self.assertEqual(progress.video.title, self.video.title)
self.assertEqual(progress.video.category.name, self.video.category.name)
self.assertEqual(progress.progress, 150)
self.assertEqual(progress.status, 'in_progress')
self.assertEqual(progress.status, "in_progress")
def test_many_progress_serializer(self):
categories = [VideoCategory.objects.create(name='Category One'),
VideoCategory.objects.create(name='Category Two')]
categories = [
VideoCategory.objects.create(name="Category One"),
VideoCategory.objects.create(name="Category Two"),
]
videos = [
Video.objects.create(
category=categories[0],
title='Test Video 1',
link='https://example.com/video1',
duration=300
title="Test Video 1",
link="https://example.com/video1",
duration=300,
),
Video.objects.create(
category=categories[0],
title='Test Video 2',
link='https://example.com/video2',
duration=300
title="Test Video 2",
link="https://example.com/video2",
duration=300,
),
Video.objects.create(
category=categories[0],
title='Test Video 3',
link='https://example.com/video3',
duration=300
title="Test Video 3",
link="https://example.com/video3",
duration=300,
),
Video.objects.create(
category=categories[1],
title='Test Video 4',
link='https://example.com/video4',
duration=300
)
title="Test Video 4",
link="https://example.com/video4",
duration=300,
),
]
progress_data = [
UserVideoProgress.objects.create(
video=videos[0],
user=self.user,
progress=100
video=videos[0], user=self.user, progress=100
),
UserVideoProgress.objects.create(
video=videos[1],
user=self.user,
progress=30
video=videos[1], user=self.user, progress=30
),
UserVideoProgress.objects.create(
video=videos[2],
user=self.user,
progress=0
video=videos[2], user=self.user, progress=0
),
UserVideoProgress.objects.create(
video=videos[3],
user=self.user,
progress=0
video=videos[3], user=self.user, progress=0
),
]
serializer = UserVideoProgressSerializer(UserVideoProgress.objects.filter(user=self.user), many=True)
serializer = UserVideoProgressSerializer(
UserVideoProgress.objects.filter(user=self.user), many=True
)
self.assertEqual(len(serializer.data), len(progress_data))
for i in range(len(serializer.data)):
self.assertEqual(serializer.data[i]['video']['title'], progress_data[i].video.title)
self.assertEqual(serializer.data[i]['video']['description'], progress_data[i].video.description)
self.assertEqual(serializer.data[i]['video']['link'], progress_data[i].video.link)
self.assertEqual(serializer.data[i]['video']['duration'], progress_data[i].video.duration)
self.assertEqual(
serializer.data[i]["video"]["title"], progress_data[i].video.title
)
self.assertEqual(
serializer.data[i]["video"]["description"],
progress_data[i].video.description,
)
self.assertEqual(
serializer.data[i]["video"]["link"], progress_data[i].video.link
)
self.assertEqual(
serializer.data[i]["video"]["duration"], progress_data[i].video.duration
)
class ConversationSerializerTest(TestCase):
def setUp(self):
self.owner_user = User.objects.create_user(
email='owner@example.com',
first_name='Property',
last_name='Owner',
user_type='property_owner',
password='testpass123'
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user(
email='vendor@example.com',
first_name='Vendor',
last_name='User',
user_type='vendor',
password='testpass123'
email="vendor@example.com",
first_name="Vendor",
last_name="User",
user_type="vendor",
password="testpass123",
)
self.vendor = Vendor.objects.create(
user=self.vendor_user,
business_name='Test Vendor',
business_type='contractor'
business_name="Test Vendor",
business_type="contractor",
)
self.property = Property.objects.create(
owner=self.owner,
address='123 Main St',
city='Anytown',
state='CA',
zip_code='90210',
market_value=500000.00
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
)
self.conversation_data = {
'property_owner': self.owner.pk,
'vendor': self.vendor.pk,
'property': self.property.pk
"property_owner": self.owner.pk,
"vendor": self.vendor.pk,
"property": self.property.pk,
}
def test_conversation_serializer(self):
serializer = ConversationSerializer(data=self.conversation_data)
serializer = ConversationRequestSerializer(data=self.conversation_data)
self.assertTrue(serializer.is_valid(), serializer.errors)
conversation = serializer.save()
self.assertEqual(conversation.property_owner, self.owner)
self.assertEqual(conversation.vendor, self.vendor)
self.assertEqual(conversation.property, self.property)
class MessageSerializerTest(TestCase):
def setUp(self):
self.owner_user = User.objects.create_user(
email='owner@example.com',
first_name='Property',
last_name='Owner',
user_type='property_owner',
password='testpass123'
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user(
email='vendor@example.com',
first_name='Vendor',
last_name='User',
user_type='vendor',
password='testpass123'
email="vendor@example.com",
first_name="Vendor",
last_name="User",
user_type="vendor",
password="testpass123",
)
self.vendor = Vendor.objects.create(
user=self.vendor_user,
business_name='Test Vendor',
business_type='contractor'
business_name="Test Vendor",
business_type="contractor",
)
self.property = Property.objects.create(
owner=self.owner,
address='123 Main St',
city='Anytown',
state='CA',
zip_code='90210',
market_value=500000.00
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
)
self.conversation = Conversation.objects.create(
property_owner=self.owner,
vendor=self.vendor,
property=self.property
property_owner=self.owner, vendor=self.vendor, property=self.property
)
self.message_data = {
'text': 'Test message'
}
self.message_data = {"text": "Test message"}
def test_message_serializer(self):
context = {'sender': self.owner_user.pk, 'conversation': self.conversation.pk, 'text': 'Test message'}
context = {
"sender": self.owner_user.pk,
"conversation": self.conversation.pk,
"text": "Test message",
}
serializer = MessageSerializer(data=context)
self.assertTrue(serializer.is_valid(), serializer.errors)
message = serializer.save()
self.assertEqual(message.conversation, self.conversation)
self.assertEqual(message.sender, self.owner_user)
self.assertEqual(message.text, 'Test message')
self.assertEqual(message.text, "Test message")
self.assertFalse(message.read)
class PasswordResetRequestSerializerTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='user@example.com',
first_name='Test',
last_name='User',
user_type='property_owner',
password='testpass123'
email="user@example.com",
first_name="Test",
last_name="User",
user_type="property_owner",
password="testpass123",
)
self.valid_data = {'email': 'user@example.com'}
self.invalid_data = {'email': 'nonexistent@example.com'}
self.valid_data = {"email": "user@example.com"}
self.invalid_data = {"email": "nonexistent@example.com"}
def test_valid_email(self):
serializer = PasswordResetRequestSerializer(data=self.valid_data)
self.assertTrue(serializer.is_valid())
def test_invalid_email(self):
serializer = PasswordResetRequestSerializer(data=self.invalid_data)
with self.assertRaises(ValidationError):
serializer.is_valid(raise_exception=True)
class PasswordResetConfirmSerializerTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email='user@example.com',
first_name='Test',
last_name='User',
user_type='property_owner',
password='testpass123'
email="user@example.com",
first_name="Test",
last_name="User",
user_type="property_owner",
password="testpass123",
)
self.token = uuid.uuid4()
self.valid_data = {
'token': self.token,
'new_password': 'newpass123',
'new_password2': 'newpass123'
"token": self.token,
"new_password": "newpass123",
"new_password2": "newpass123",
}
self.mismatch_data = {
'token': self.token,
'new_password': 'newpass123',
'new_password2': 'differentpass'
"token": self.token,
"new_password": "newpass123",
"new_password2": "differentpass",
}
def test_valid_password_reset(self):
PasswordResetToken.objects.create(
user=self.user,
token=self.token,
expires_at=datetime.now() + timedelta(hours=24)
expires_at=datetime.now() + timedelta(hours=24),
)
serializer = PasswordResetConfirmSerializer(data=self.valid_data)
self.assertTrue(serializer.is_valid())
def test_password_mismatch(self):
serializer = PasswordResetConfirmSerializer(data=self.mismatch_data)
with self.assertRaises(ValidationError):
serializer.is_valid(raise_exception=True)
class OfferSerializerTest(TestCase):
def setUp(self):
self.owner_user = User.objects.create_user(
email="owner@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.property = Property.objects.create(
owner=self.owner,
address="78 Midgewood",
city="Boardman",
state="OH",
zip_code="44512",
market_value=500000.00,
)
self.owner_user_2 = User.objects.create_user(
email="owner2@example.com",
first_name="Property",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.owner_2 = PropertyOwner.objects.create(user=self.owner_user_2)
self.property_2 = Property.objects.create(
owner=self.owner_2,
address="1968 Greensboro Dr",
city="wheaton",
state="IL",
zip_code="60189",
market_value=500000.00,
)
self.offer_user = User.objects.create_user(
email="vendor@example.com",
first_name="Vendor",
last_name="User",
user_type="vendor",
password="testpass123",
)
def test_create_offer_serializer(self):
context = {"user": self.offer_user.pk, "property": self.property.pk}
serializer = OfferRequestSerializer(data=context)
self.assertTrue(serializer.is_valid(), serializer.errors)
offer = serializer.save()
self.assertEqual(offer.user, self.offer_user)
self.assertEqual(offer.status, "draft")
self.assertEqual(offer.property, self.property)
def test_response_offer_serializer(self):
offer = Offer.objects.create(
user=self.offer_user,
property=self.property,
)
serializer = OfferResponseSerializer(offer)
self.assertTrue(serializer.data["status"] == "draft", serializer.data)
self.assertTrue(serializer.data["is_active"], serializer.data)
self.assertTrue(serializer.data["user"], self.offer_user.pk)
self.assertTrue(serializer.data["property"], self.property.pk)

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,72 @@
annotated-types==0.7.0
anyio==4.9.0
asgiref==3.9.0
attrs==25.3.0
autobahn==24.4.2
Automat==25.4.16
black==25.1.0
certifi==2025.7.14
cffi==1.17.1
cfgv==3.4.0
channels==4.2.2
channels_redis==4.2.1
charset-normalizer==3.4.2
click==8.2.1
constantly==23.10.4
coverage==7.9.2
cryptography==45.0.6
daphne==4.2.1
distlib==0.3.9
Django==5.2.4
django-cors-headers==4.7.0
django-filter==25.1
djangorestframework==3.16.0
djangorestframework_simplejwt==5.5.0
filelock==3.18.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
hyperlink==21.0.0
identify==2.6.12
idna==3.10
incremental==24.7.2
jsonpatch==1.33
jsonpointer==3.0.0
langchain-core==0.3.72
langchain-ollama==0.3.6
langsmith==0.4.9
msgpack==1.1.1
mypy_extensions==1.1.0
nodeenv==1.9.1
ollama==0.5.1
orjson==3.11.1
packaging==25.0
pathspec==0.12.1
pillow==11.3.0
platformdirs==4.3.8
pre_commit==4.2.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.22
pydantic==2.11.7
pydantic_core==2.33.2
PyJWT==2.9.0
pyOpenSSL==25.1.0
python-decouple==3.8
PyYAML==6.0.2
redis==6.2.0
requests==2.32.4
requests-toolbelt==1.0.0
service-identity==24.2.0
setuptools==80.9.0
sniffio==1.3.1
sqlparse==0.5.3
tenacity==9.1.2
Twisted==25.5.0
txaio==25.6.1
typing-inspection==0.4.1
typing_extensions==4.14.1
urllib3==2.5.0
virtualenv==20.31.2
zope.interface==7.2
zstandard==0.23.0