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 .env
db.sqlite3 db.sqlite3
media/

View File

@@ -1,3 +1,210 @@
from django.contrib import admin 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. # 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): class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'core' name = "core"

View File

@@ -5,32 +5,38 @@ from django.contrib.auth import get_user_model
from rest_framework_simplejwt.tokens import AccessToken from rest_framework_simplejwt.tokens import AccessToken
from .models import Conversation, Message from .models import Conversation, Message
from .serializers import MessageSerializer from .serializers import MessageSerializer
from .services.moderation_classifier import moderation_classifier, ModerationLabel
from .services.llm_service import AsyncLLMService
User = get_user_model() User = get_user_model()
class ChatConsumer(AsyncWebsocketConsumer): class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self): async def connect(self):
self.conversation_id = self.scope['url_route']['kwargs']['conversation_id'] print(self.scope)
self.conversation_group_name = f'chat_{self.conversation_id}'
self.account_id = self.scope["url_route"]["kwargs"]["account_id"]
self.account_group_name = f"chat_{self.account_id}"
# Authenticate user via JWT # 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: try:
access_token = AccessToken(token) access_token = AccessToken(token)
user_id = access_token['user_id'] user_id = access_token["user_id"]
self.user = await self.get_user(user_id) self.user = await self.get_user(user_id)
# Check if user is part of the conversation # # Check if user is part of the conversation
conversation = await self.get_conversation(self.conversation_id) # conversation = await self.get_conversation(self.conversation_id)
if not await self.is_participant(conversation, self.user): # if not await self.is_participant(conversation, self.user):
await self.close() # await self.close()
return # return
#
await self.channel_layer.group_add(
self.conversation_group_name, # await self.channel_layer.group_add(
self.channel_name # self.account_group_name, self.account_id
) # )
await self.accept() await self.accept()
except Exception as e: except Exception as e:
@@ -38,38 +44,63 @@ class ChatConsumer(AsyncWebsocketConsumer):
await self.close() await self.close()
async def disconnect(self, close_code): async def disconnect(self, close_code):
if (self.channel_layer):
await self.channel_layer.group_discard( await self.channel_layer.group_discard(
self.conversation_group_name, self.account_group_name, self.account_id
self.channel_name
) )
async def receive(self, text_data): async def receive(self, text_data):
text_data_json = json.loads(text_data) text_data_json = json.loads(text_data)
message = text_data_json['message'] print(text_data_json)
"""
First see if it is NSFW
# Save message to database Then to the conversation
conversation = await self.get_conversation(self.conversation_id) """
message_obj = await self.create_message(conversation, self.user, message) 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},
# )
# 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
}
)
async def chat_message(self, event): async def chat_message(self, event):
message = event['message'] message = event["message"]
# Send message to WebSocket # Send message to WebSocket
await self.send(text_data=json.dumps({ await self.send(text_data=json.dumps({"message": message}))
'message': message
}))
@database_sync_to_async @database_sync_to_async
def get_user(self, user_id): def get_user(self, user_id):
@@ -81,16 +112,12 @@ class ChatConsumer(AsyncWebsocketConsumer):
@database_sync_to_async @database_sync_to_async
def is_participant(self, conversation, user): def is_participant(self, conversation, user):
if user.user_type == 'property_owner': if user.user_type == "property_owner":
return conversation.property_owner.user == user return conversation.property_owner.user == user
elif user.user_type == 'vendor': elif user.user_type == "vendor":
return conversation.vendor.user == user return conversation.vendor.user == user
return False return False
@database_sync_to_async @database_sync_to_async
def create_message(self, conversation, user, text): def create_message(self, conversation, user, text):
return Message.objects.create( return Message.objects.create(conversation=conversation, sender=user, text=text)
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 initial = True
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ("auth", "0012_alter_user_first_name_max_length"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='User', name="User",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('password', models.CharField(max_length=128, verbose_name='password')), "id",
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), models.BigAutoField(
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), auto_created=True,
('email', models.EmailField(max_length=254, unique=True)), primary_key=True,
('first_name', models.CharField(max_length=30)), serialize=False,
('last_name', models.CharField(max_length=30)), verbose_name="ID",
('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)), ("password", models.CharField(max_length=128, verbose_name="password")),
('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')), "last_login",
('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')), 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={ options={
'abstract': False, "abstract": False,
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Conversation', name="Conversation",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('created_at', models.DateTimeField(auto_now_add=True)), "id",
('updated_at', models.DateTimeField(auto_now=True)), 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( migrations.CreateModel(
name='Property', name="Property",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('address', models.CharField(max_length=200)), "id",
('city', models.CharField(max_length=100)), models.BigAutoField(
('state', models.CharField(max_length=2)), auto_created=True,
('zip_code', models.CharField(max_length=10)), primary_key=True,
('market_value', models.DecimalField(decimal_places=2, max_digits=12)), serialize=False,
('loan_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), verbose_name="ID",
('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)), ("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( migrations.CreateModel(
name='VideoCategory', name="VideoCategory",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100)), "id",
('description', models.TextField(blank=True, null=True)), 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( migrations.CreateModel(
name='PropertyOwner', name="PropertyOwner",
fields=[ 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( migrations.CreateModel(
name='Vendor', name="Vendor",
fields=[ 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)), "user",
('business_type', models.CharField(choices=[('contractor', 'Contractor'), ('inspector', 'Inspector'), ('lender', 'Lender'), ('other', 'Other')], max_length=20)), models.OneToOneField(
('phone_number', models.CharField(blank=True, max_length=20, null=True)), on_delete=django.db.models.deletion.CASCADE,
('address', models.CharField(max_length=200)), primary_key=True,
('city', models.CharField(max_length=100)), serialize=False,
('state', models.CharField(max_length=2)), to=settings.AUTH_USER_MODEL,
('zip_code', models.CharField(max_length=10)), ),
),
("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( migrations.CreateModel(
name='Message', name="Message",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('text', models.TextField()), "id",
('attachment', models.FileField(blank=True, null=True, upload_to=core.models.message_file_path)), models.BigAutoField(
('timestamp', models.DateTimeField(auto_now_add=True)), auto_created=True,
('read', models.BooleanField(default=False)), primary_key=True,
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='core.conversation')), serialize=False,
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)), 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( migrations.CreateModel(
name='PasswordResetToken', name="PasswordResetToken",
fields=[ 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)), "id",
('created_at', models.DateTimeField(auto_now_add=True)), models.BigAutoField(
('expires_at', models.DateTimeField()), auto_created=True,
('used', models.BooleanField(default=False)), primary_key=True,
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 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( migrations.AddField(
model_name='conversation', model_name="conversation",
name='property', name="property",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.property'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="conversations",
to="core.property",
),
), ),
migrations.CreateModel( migrations.CreateModel(
name='Video', name="Video",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('title', models.CharField(max_length=200)), "id",
('description', models.TextField(blank=True, null=True)), models.BigAutoField(
('link', models.URLField()), auto_created=True,
('duration', models.IntegerField(help_text='Duration in seconds')), primary_key=True,
('created_at', models.DateTimeField(auto_now_add=True)), serialize=False,
('updated_at', models.DateTimeField(auto_now=True)), verbose_name="ID",
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='videos', to='core.videocategory')), ),
),
("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( migrations.AddField(
model_name='property', model_name="property",
name='owner', name="owner",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='core.propertyowner'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="properties",
to="core.propertyowner",
),
), ),
migrations.AddField( migrations.AddField(
model_name='conversation', model_name="conversation",
name='property_owner', name="property_owner",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.propertyowner'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="conversations",
to="core.propertyowner",
),
), ),
migrations.AddField( migrations.AddField(
model_name='conversation', model_name="conversation",
name='vendor', name="vendor",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.vendor'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="conversations",
to="core.vendor",
),
), ),
migrations.CreateModel( migrations.CreateModel(
name='UserVideoProgress', name="UserVideoProgress",
fields=[ 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')), "id",
('status', models.CharField(choices=[('not_started', 'Not Started'), ('in_progress', 'In Progress'), ('completed', 'Completed')], default='not_started', max_length=20)), models.BigAutoField(
('last_watched', models.DateTimeField(auto_now=True)), auto_created=True,
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='video_progress', to=settings.AUTH_USER_MODEL)), primary_key=True,
('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_progress', to='core.video')), 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={ options={
'unique_together': {('user', 'video')}, "unique_together": {("user", "video")},
}, },
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='conversation', name="conversation",
unique_together={('property_owner', 'vendor', 'property')}, unique_together={("property_owner", "vendor", "property")},
), ),
] ]

View File

@@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('core', '0001_initial'), ("core", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='conversation', name="conversation",
unique_together=set(), 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.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.utils import timezone
from django.core.mail import send_mail from django.core.mail import send_mail
from django.template.loader import render_to_string from django.template.loader import render_to_string
@@ -8,32 +12,10 @@ from django.conf import settings
import uuid import uuid
import os 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): class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields): def create_user(self, email, password=None, **extra_fields):
if not email: if not email:
raise ValueError('Users must have an email address') raise ValueError("Users must have an email address")
email = self.normalize_email(email) email = self.normalize_email(email)
user = self.model(email=email, **extra_fields) user = self.model(email=email, **extra_fields)
@@ -42,15 +24,23 @@ class UserManager(BaseUserManager):
return user return user
def create_superuser(self, email, password=None, **extra_fields): def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True) extra_fields.setdefault("is_staff", True)
extra_fields.setdefault('is_superuser', True) extra_fields.setdefault("is_superuser", True)
return self.create_user(email, password, **extra_fields) return self.create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin): class User(AbstractBaseUser, PermissionsMixin):
USER_TYPE_CHOICES = ( USER_TYPE_CHOICES = (
('property_owner', 'Property Owner'), ("property_owner", "Property Owner"),
('vendor', 'Vendor'), ("vendor", "Vendor"),
('admin', 'Admin'), ("attorney", "Attorney"),
("real_estate_agent", "Real Estate Agent"),
("admin", "Admin"),
)
USER_TIER_CHOICES = (
("basic", "Basic"),
("premium", "Premium"),
("vendor", "Vendor"),
) )
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
@@ -60,11 +50,16 @@ class User(AbstractBaseUser, PermissionsMixin):
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False) is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(default=timezone.now) 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() objects = UserManager()
USERNAME_FIELD = 'email' USERNAME_FIELD = "email"
REQUIRED_FIELDS = ['first_name', 'last_name', 'user_type'] REQUIRED_FIELDS = ["first_name", "last_name", "user_type"]
def __str__(self): def __str__(self):
return self.email return self.email
@@ -75,19 +70,25 @@ class User(AbstractBaseUser, PermissionsMixin):
def get_short_name(self): def get_short_name(self):
return self.first_name return self.first_name
class PropertyOwner(models.Model): class PropertyOwner(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
phone_number = models.CharField(max_length=20, blank=True, null=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): def __str__(self):
return self.user.get_full_name() return self.user.get_full_name()
class Vendor(models.Model): class Vendor(models.Model):
BUSINESS_TYPES = ( BUSINESS_TYPES = (
('contractor', 'Contractor'), ("electrician", "Electrician"),
('inspector', 'Inspector'), ("carpenter", "Carpenter"),
('lender', 'Lender'), ("plumber", "Plumber"),
('other', 'Other'), ("inspector", "Inspector"),
("lender", "Lender"),
("other", "Other"),
) )
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
@@ -98,37 +99,256 @@ class Vendor(models.Model):
city = models.CharField(max_length=100) city = models.CharField(max_length=100)
state = models.CharField(max_length=2) state = models.CharField(max_length=2)
zip_code = models.CharField(max_length=10) zip_code = models.CharField(max_length=10)
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): def __str__(self):
return self.business_name return self.business_name
class Property(models.Model): class Attorney(models.Model):
owner = models.ForeignKey(PropertyOwner, on_delete=models.CASCADE, related_name='properties') 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) address = models.CharField(max_length=200)
city = models.CharField(max_length=100) city = models.CharField(max_length=100)
state = models.CharField(max_length=2) state = models.CharField(max_length=2)
zip_code = models.CharField(max_length=10) zip_code = models.CharField(max_length=10)
specialties = models.JSONField(blank=True, default=list) # Store as JSON array
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) 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_amount = models.DecimalField(
loan_interest_rate = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True) 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_term = models.IntegerField(blank=True, null=True)
loan_start_date = models.DateField(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): def __str__(self):
return f"{self.address}, {self.city}, {self.state} {self.zip_code}" return f"{self.address}, {self.city}, {self.state} {self.zip_code}"
class SchoolInfo(models.Model):
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): class VideoCategory(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return self.name return self.name
class Video(models.Model): 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) title = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
link = models.URLField() link = models.FileField(upload_to="videos/")
duration = models.IntegerField(help_text="Duration in seconds") duration = models.IntegerField(help_text="Duration in seconds")
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -136,21 +356,28 @@ class Video(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
class UserVideoProgress(models.Model): class UserVideoProgress(models.Model):
STATUS_CHOICES = ( STATUS_CHOICES = (
('not_started', 'Not Started'), ("not_started", "Not Started"),
('in_progress', 'In Progress'), ("in_progress", "In Progress"),
('completed', 'Completed'), ("completed", "Completed"),
) )
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='video_progress') user = models.ForeignKey(
video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='user_progress') 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") 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) last_watched = models.DateTimeField(auto_now=True)
class Meta: class Meta:
unique_together = ('user', 'video') unique_together = ("user", "video")
def __str__(self): def __str__(self):
return f"{self.user.email} - {self.video.title} - {self.status}" return f"{self.user.email} - {self.video.title} - {self.status}"
@@ -158,18 +385,29 @@ class UserVideoProgress(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Update status based on progress # Update status based on progress
if self.progress == 0: if self.progress == 0:
self.status = 'not_started' self.status = "not_started"
elif self.progress >= self.video.duration: elif self.progress >= self.video.duration:
self.status = 'completed' self.status = "completed"
self.progress = self.video.duration self.progress = self.video.duration
else: else:
self.status = 'in_progress' self.status = "in_progress"
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Conversation(models.Model): class Conversation(models.Model):
property_owner = models.ForeignKey(PropertyOwner, on_delete=models.CASCADE, related_name='conversations') property_owner = models.ForeignKey(
vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='conversations') PropertyOwner, on_delete=models.CASCADE, related_name="conversations"
property = models.ForeignKey(Property, 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) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -179,14 +417,20 @@ class Conversation(models.Model):
def __str__(self): def __str__(self):
return f"Conversation between {self.property_owner} and {self.vendor} about {self.property}" return f"Conversation between {self.property_owner} and {self.vendor} about {self.property}"
def message_file_path(instance, filename): def message_file_path(instance, filename):
ext = filename.split('.')[-1] ext = filename.split(".")[-1]
filename = f"{uuid.uuid4()}.{ext}" filename = f"{uuid.uuid4()}.{ext}"
return os.path.join('message_attachments', filename) return os.path.join("message_attachments", filename)
class Message(models.Model): class Message(models.Model):
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages') conversation = models.ForeignKey(
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages') Conversation, on_delete=models.CASCADE, related_name="messages"
)
sender = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="sent_messages"
)
text = models.TextField() text = models.TextField()
attachment = models.FileField(upload_to=message_file_path, blank=True, null=True) attachment = models.FileField(upload_to=message_file_path, blank=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
@@ -195,6 +439,7 @@ class Message(models.Model):
def __str__(self): def __str__(self):
return f"Message from {self.sender} in {self.conversation}" return f"Message from {self.sender} in {self.conversation}"
class PasswordResetToken(models.Model): class PasswordResetToken(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
@@ -209,10 +454,10 @@ class PasswordResetToken(models.Model):
subject = "Password Reset Request" subject = "Password Reset Request"
reset_url = f"{settings.FRONTEND_URL}/reset-password/{self.token}/" reset_url = f"{settings.FRONTEND_URL}/reset-password/{self.token}/"
context = { context = {
'user': self.user, "user": self.user,
'reset_url': reset_url, "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) plain_message = strip_tags(html_message)
send_mail( send_mail(
subject, subject,
@@ -225,3 +470,87 @@ class PasswordResetToken(models.Model):
def __str__(self): 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 from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission): class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS: if request.method in permissions.SAFE_METHODS:
return True return True
if hasattr(obj, 'owner'): if hasattr(obj, "owner"):
return obj.owner.user == request.user return obj.owner.user == request.user
elif hasattr(obj, 'user'): elif hasattr(obj, "user"):
return obj.user == request.user return obj.user == request.user
return False return False
class IsPropertyOwner(permissions.BasePermission): class IsPropertyOwner(permissions.BasePermission):
def has_permission(self, request, view): 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): def has_object_permission(self, request, view, obj):
if hasattr(obj, 'owner'): if hasattr(obj, "owner"):
return obj.owner.user == request.user return obj.owner.user == request.user
elif hasattr(obj, 'property_owner'): elif hasattr(obj, "property_owner"):
return obj.property_owner.user == request.user return obj.property_owner.user == request.user
return False return False
class IsVendor(permissions.BasePermission): class IsVendor(permissions.BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user.user_type == 'vendor' return request.user.user_type == "vendor"
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
if hasattr(obj, 'vendor'): if hasattr(obj, "vendor"):
return obj.vendor.user == request.user return obj.vendor.user == request.user
return False return False
class IsParticipant(permissions.BasePermission): class IsParticipant(permissions.BasePermission):
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
if request.user.user_type == 'property_owner': if request.user.user_type == "property_owner":
owner = obj.property_owner if hasattr(obj, 'property_owner') else obj.conversation.property_owner owner = (
obj.property_owner
if hasattr(obj, "property_owner")
else obj.conversation.property_owner
)
return owner.user == request.user return owner.user == request.user
elif request.user.user_type == 'vendor': elif request.user.user_type == "vendor":
vendor = obj.vendor if hasattr(obj, 'vendor') else obj.conversation.vendor vendor = obj.vendor if hasattr(obj, "vendor") else obj.conversation.vendor
return vendor.user == request.user 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 from . import consumers
websocket_urlpatterns = [ 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.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from .models import ( from .models import (
PropertyOwner, Vendor, Property, VideoCategory, Video, PropertyOwner,
UserVideoProgress, Conversation, Message, PasswordResetToken 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.core.mail import send_mail
from django.template.loader import render_to_string from django.template.loader import render_to_string
@@ -15,16 +30,17 @@ from datetime import datetime, timedelta
User = get_user_model() User = get_user_model()
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod @classmethod
def get_token(cls, user): def get_token(cls, user):
token = super().get_token(user) token = super().get_token(user)
# Add custom claims # Add custom claims
token['email'] = user.email token["email"] = user.email
token['first_name'] = user.first_name token["first_name"] = user.first_name
token['last_name'] = user.last_name token["last_name"] = user.last_name
token['user_type'] = user.user_type token["user_type"] = user.user_type
return token return token
@@ -33,24 +49,37 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
# Add additional responses # Add additional responses
refresh = self.get_token(self.user) refresh = self.get_token(self.user)
data['refresh'] = str(refresh) data["refresh"] = str(refresh)
data['access'] = str(refresh.access_token) data["access"] = str(refresh.access_token)
# Add user details # Add user details
data['user'] = { data["user"] = {
'email': self.user.email, "email": self.user.email,
'first_name': self.user.first_name, "first_name": self.user.first_name,
'last_name': self.user.last_name, "last_name": self.user.last_name,
'user_type': self.user.user_type, "user_type": self.user.user_type,
} }
return data return data
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['id', 'email', 'first_name', 'last_name', 'user_type', 'is_active', 'date_joined'] fields = [
read_only_fields = ['id', 'is_active', 'date_joined'] "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): class UserRegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True) password = serializers.CharField(write_only=True, required=True)
@@ -58,37 +87,48 @@ class UserRegisterSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['email', 'first_name', 'last_name', 'user_type', 'password', 'password2'] fields = [
"email",
"first_name",
"last_name",
"user_type",
"password",
"password2",
]
extra_kwargs = { extra_kwargs = {
'password': {'write_only': True}, "password": {"write_only": True},
'password2': {'write_only': True}, "password2": {"write_only": True},
} }
def validate(self, attrs): def validate(self, attrs):
if attrs['password'] != attrs['password2']: if attrs["password"] != attrs["password2"]:
raise serializers.ValidationError({"password": "Password fields didn't match."}) raise serializers.ValidationError(
{"password": "Password fields didn't match."}
)
return attrs return attrs
def create(self, validated_data): def create(self, validated_data):
validated_data.pop('password2') validated_data.pop("password2")
user = User.objects.create_user(**validated_data) user = User.objects.create_user(**validated_data)
return user return user
class PropertyOwnerSerializer(serializers.ModelSerializer): class PropertyOwnerSerializer(serializers.ModelSerializer):
user = UserSerializer() user = UserSerializer()
class Meta: class Meta:
model = PropertyOwner model = PropertyOwner
fields = ['user', 'phone_number'] fields = ["user", "phone_number"]
read_only_fields = ["created_at", "updated_at"]
def create(self, validated_data): def create(self, validated_data):
user_data = validated_data.pop('user') user_data = validated_data.pop("user")
user = User.objects.create_user(**user_data) user = User.objects.create_user(**user_data)
property_owner = PropertyOwner.objects.create(user=user, **validated_data) property_owner = PropertyOwner.objects.create(user=user, **validated_data)
return property_owner return property_owner
def update(self, instance, validated_data): def update(self, instance, validated_data):
user_data = validated_data.pop('user', None) user_data = validated_data.pop("user", None)
if user_data: if user_data:
user = instance.user user = instance.user
for attr, value in user_data.items(): for attr, value in user_data.items():
@@ -100,77 +140,320 @@ class PropertyOwnerSerializer(serializers.ModelSerializer):
instance.save() instance.save()
return instance return instance
class VendorSerializer(serializers.ModelSerializer): class VendorSerializer(serializers.ModelSerializer):
user = UserSerializer() user = UserSerializer()
class Meta: class Meta:
model = Vendor model = Vendor
fields = ['user', 'business_name', 'business_type', 'phone_number', fields = [
'address', 'city', 'state', 'zip_code'] # 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): def create(self, validated_data):
# Extract user data user_data = validated_data.pop("user")
user_data = validated_data.pop('user') user = User.objects.create_user(**user_data) # Use create_user to hash the password if present
# Get or create category
user, _ = User.objects.get_or_create(**user_data)
# Create video with the category
vendor = Vendor.objects.create(user=user, **validated_data) vendor = Vendor.objects.create(user=user, **validated_data)
return vendor return vendor
# Override the update method to handle the nested user data
def update(self, instance, validated_data): def update(self, instance, validated_data):
user_data = validated_data.pop('user', None) # Pop the user data to handle it separately
user_data = validated_data.pop("user", {})
user_instance = instance.user
# Update Vendor fields # Update the Vendor instance fields
for attr, value in validated_data.items(): for attr, value in validated_data.items():
setattr(instance, attr, value) setattr(instance, attr, value)
instance.save() instance.save()
# Update nested User fields if provided # Update the nested User instance fields
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(): for attr, value in user_data.items():
setattr(user, attr, value) setattr(user_instance, attr, value)
user.save() user_instance.save()
return instance 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: class Meta:
model = Property model = Property
fields = ['id', 'owner', 'address', 'city', 'state', 'zip_code', fields = [
'market_value', 'loan_amount', 'loan_interest_rate', "id",
'loan_term', 'loan_start_date'] "owner",
read_only_fields = ['id'] "address",
"street",
"city",
"state",
"zip_code",
"market_value",
"loan_amount",
"loan_interest_rate",
"loan_term",
"loan_start_date",
"created_at",
"updated_at",
"description",
"sq_ft",
"features",
"num_bedrooms",
"num_bathrooms",
"latitude",
"longitude",
"realestate_api_id",
"property_status",
"views",
"saves",
"listed_date",
"pictures",
'open_houses',
'schools',
'walk_score',
'tax_info',
'sale_info',
]
read_only_fields = ["id", "created_at", "updated_at"]
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 VideoCategorySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = VideoCategory model = VideoCategory
fields = ['id', 'name', 'description'] fields = ["id", "name", "description"]
read_only_fields = ['id'] read_only_fields = ["id"]
class VideoSerializer(serializers.ModelSerializer): class VideoSerializer(serializers.ModelSerializer):
category = VideoCategorySerializer() category = VideoCategorySerializer()
class Meta: class Meta:
model = Video model = Video
fields = ['id', 'category', 'title', 'description', 'link', 'duration', 'created_at', 'updated_at'] fields = [
read_only_fields = ['id', 'created_at', 'updated_at'] "id",
"category",
"title",
"description",
"link",
"duration",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
def create(self, validated_data): def create(self, validated_data):
# Extract category data # Extract category data
category_data = validated_data.pop('category') category_data = validated_data.pop("category")
# Get or create category # Get or create category
category, _ = VideoCategory.objects.get_or_create(**category_data) category, _ = VideoCategory.objects.get_or_create(**category_data)
@@ -181,7 +464,7 @@ class VideoSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
# Handle category update if provided # Handle category update if provided
category_data = validated_data.pop('category', None) category_data = validated_data.pop("category", None)
if category_data: if category_data:
category, _ = VideoCategory.objects.get_or_create(**category_data) category, _ = VideoCategory.objects.get_or_create(**category_data)
instance.category = category instance.category = category
@@ -193,91 +476,128 @@ class VideoSerializer(serializers.ModelSerializer):
instance.save() instance.save()
return instance return instance
class UserVideoProgressSerializer(serializers.ModelSerializer): class UserVideoProgressSerializer(serializers.ModelSerializer):
video = VideoSerializer() video = VideoSerializer()
class Meta: class Meta:
model = UserVideoProgress model = UserVideoProgress
fields = ['video', 'progress', 'status', 'last_watched', 'user'] fields = ["id", "video", "progress", "status", "last_watched", "user"]
read_only_fields = ['status', 'last_watched'] read_only_fields = ["id", "status", "last_watched"]
def create(self, validated_data): def create(self, validated_data):
# Extract video data # Extract video data
video_data = validated_data.pop('video') video_data = validated_data.pop("video")
# Get or create video # Get or create video
video_serializer = VideoSerializer(data=video_data) video_serializer = VideoSerializer(data=video_data)
video_serializer.is_valid(raise_exception=True) video_serializer.is_valid(raise_exception=True)
video = video_serializer.save() video = video_serializer.save()
user = validated_data.pop('user') user = validated_data.pop("user")
# Create progress record # Create progress record
progress = UserVideoProgress.objects.create( progress = UserVideoProgress.objects.create(
user=user, user=user, video=video, **validated_data
video=video,
**validated_data
) )
return progress return progress
def update(self, instance, validated_data): def update(self, instance, validated_data):
# Handle video update if provided # Handle video update if provided
video_data = validated_data.pop('video', None) video_data = validated_data.pop("video", None)
if video_data: 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.is_valid(raise_exception=True)
video_serializer.save() video_serializer.save()
# Update progress # Update progress
instance.progress = validated_data.get('progress', instance.progress) instance.progress = validated_data.get("progress", instance.progress)
instance.save() instance.save()
return instance 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 MessageSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Message model = Message
fields = ['id', 'conversation', 'sender', 'text', 'attachment', 'timestamp', 'read'] fields = [
read_only_fields = ['id', 'timestamp'] "id",
"conversation",
"sender",
"text",
"attachment",
"timestamp",
"read",
]
read_only_fields = ["id", "timestamp"]
def create(self, validated_data): def create(self, validated_data):
# Extract user data # Extract user data
sender = validated_data.pop('sender') sender = validated_data.pop("sender")
message = Message.objects.create(sender=sender, **validated_data) message = Message.objects.create(sender=sender, **validated_data)
return message return message
def update(self, instance, validated_data): def update(self, instance, validated_data):
""" """
Handle updates to message fields. Handle updates to message fields.
Note: sender and conversation are typically read-only in updates Note: sender and conversation are typically read-only in updates
""" """
# Update text if provided # 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 # Update read status if provided
if 'read' in validated_data: if "read" in validated_data:
instance.read = validated_data['read'] instance.read = validated_data["read"]
# Handle attachment updates (if needed) # Handle attachment updates (if needed)
if 'attachment' in validated_data: if "attachment" in validated_data:
# Delete old attachment if exists # Delete old attachment if exists
if instance.attachment: if instance.attachment:
instance.attachment.delete() instance.attachment.delete()
instance.attachment = validated_data['attachment'] instance.attachment = validated_data["attachment"]
instance.save() instance.save()
return instance 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): class PasswordResetRequestSerializer(serializers.Serializer):
email = serializers.EmailField() email = serializers.EmailField()
@@ -289,15 +609,13 @@ class PasswordResetRequestSerializer(serializers.Serializer):
return value return value
def save(self): 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) expires_at = datetime.now() + timedelta(hours=24)
token = PasswordResetToken.objects.create( token = PasswordResetToken.objects.create(user=user, expires_at=expires_at)
user=user,
expires_at=expires_at
)
token.send_reset_email() token.send_reset_email()
return token return token
class PasswordResetConfirmSerializer(serializers.Serializer): class PasswordResetConfirmSerializer(serializers.Serializer):
token = serializers.UUIDField() token = serializers.UUIDField()
new_password = serializers.CharField(write_only=True) new_password = serializers.CharField(write_only=True)
@@ -306,23 +624,219 @@ class PasswordResetConfirmSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
try: try:
token = PasswordResetToken.objects.get(token=attrs['token']) token = PasswordResetToken.objects.get(token=attrs["token"])
except PasswordResetToken.DoesNotExist: except PasswordResetToken.DoesNotExist:
raise serializers.ValidationError({"token": "Invalid token."}) raise serializers.ValidationError({"token": "Invalid token."})
if not token.is_valid(): if not token.is_valid():
raise serializers.ValidationError({"token": "Token is invalid or has expired."}) raise serializers.ValidationError(
{"token": "Token is invalid or has expired."}
)
if attrs['new_password'] != attrs['new_password2']: if attrs["new_password"] != attrs["new_password2"]:
raise serializers.ValidationError({"new_password": "Password fields didn't match."}) raise serializers.ValidationError(
{"new_password": "Password fields didn't match."}
)
return attrs return attrs
def save(self): 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 = token.user
user.set_password(self.validated_data['new_password']) user.set_password(self.validated_data["new_password"])
user.save() user.save()
token.used = True token.used = True
token.save() 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 from core.views import ConversationViewSet, MessageViewSet
router = DefaultRouter() router = DefaultRouter()
router.register(r'', ConversationViewSet, basename='conversation') router.register(r"", ConversationViewSet, basename="conversation")
urlpatterns = [ urlpatterns = [
path('<int:conversation_id>/messages/', MessageViewSet.as_view({ path(
'get': 'list', "<int:conversation_id>/messages/",
'post': 'create' MessageViewSet.as_view({"get": "list", "post": "create"}),
}), name='conversation-messages'), name="conversation-messages",
path('<int:conversation_id>/messages/<int:pk>/', MessageViewSet.as_view({ ),
'get': 'retrieve', path(
'put': 'update', "<int:conversation_id>/messages/<int:pk>/",
'patch': 'partial_update', MessageViewSet.as_view(
'delete': 'destroy' {
}), name='message-detail'), "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 django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from core.views import PropertyViewSet from core.views import PropertyViewSet, PropertyPictureViewSet
router = DefaultRouter() 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 from core.views import PropertyOwnerViewSet
router = DefaultRouter() 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 from core.views import VendorViewSet
router = DefaultRouter() 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 from core.views import VideoCategoryViewSet, VideoViewSet, UserVideoProgressViewSet
router = DefaultRouter() router = DefaultRouter()
router.register(r'categories', VideoCategoryViewSet, basename='video-category') 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'progress', UserVideoProgressViewSet, basename='video-progress') router.register(r"", VideoViewSet, basename="video")
urlpatterns = router.urls urlpatterns = router.urls

View File

@@ -3,34 +3,103 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
import requests
from rest_framework.decorators import action
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from .models import ( from .models import (
PropertyOwner, Vendor, Property, VideoCategory, Video, PropertyOwner,
UserVideoProgress, Conversation, Message Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
Offer,
PropertyWalkScoreInfo,
PropertyTaxInfo,
SchoolInfo,Bid, BidResponse, Attorney, RealEstateAgent, UserViewModel, PropertySave
) )
from .serializers import ( from .serializers import (
CustomTokenObtainPairSerializer, UserSerializer, UserRegisterSerializer, CustomTokenObtainPairSerializer,
PropertyOwnerSerializer, VendorSerializer, PropertySerializer, UserSerializer,
VideoCategorySerializer, VideoSerializer, UserVideoProgressSerializer, UserRegisterSerializer,
ConversationSerializer, MessageSerializer, PasswordResetRequestSerializer, PropertyOwnerSerializer,
PasswordResetConfirmSerializer 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 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 django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters 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() User = get_user_model()
class CustomTokenObtainPairView(TokenObtainPairView): class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer serializer_class = CustomTokenObtainPairSerializer
class UserRegisterView(generics.CreateAPIView): class UserRegisterView(generics.CreateAPIView):
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserRegisterSerializer serializer_class = UserRegisterSerializer
permission_classes = [permissions.AllowAny] 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): class LogoutView(APIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
authentication_classes = () authentication_classes = ()
@@ -44,6 +113,7 @@ class LogoutView(APIView):
except Exception as e: except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
class PasswordResetRequestView(APIView): class PasswordResetRequestView(APIView):
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
@@ -53,10 +123,11 @@ class PasswordResetRequestView(APIView):
serializer.save() serializer.save()
return Response( return Response(
{"detail": "Password reset email has been sent."}, {"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) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PasswordResetConfirmView(APIView): class PasswordResetConfirmView(APIView):
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
@@ -66,108 +137,465 @@ class PasswordResetConfirmView(APIView):
serializer.save() serializer.save()
return Response( return Response(
{"detail": "Password has been reset successfully."}, {"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) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PropertyOwnerViewSet(viewsets.ModelViewSet): class PropertyOwnerViewSet(viewsets.ModelViewSet):
queryset = PropertyOwner.objects.all()
serializer_class = PropertyOwnerSerializer serializer_class = PropertyOwnerSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter] 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"]
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']
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): def get_queryset(self):
user = self.request.user user = self.request.user
if user.user_type == 'property_owner': if user.user_type == "property_owner":
if(PropertyOwner.objects.filter(user=user).count() == 0):
return PropertyOwner.objects.create(
user=user,
)
return PropertyOwner.objects.filter(user=user)
else:
return PropertyOwner.objects.all()
class VendorViewSet(viewsets.ModelViewSet):
serializer_class = VendorSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
search_fields = [
"business_name",
"user__first_name",
"user__last_name",
"user__email",
]
filterset_fields = ["business_type"]
lookup_field = "user__id" # or 'user__id' if you want to be explicit
def get_permissions(self):
if self.action in ['increment_view_count', 'increment_save_count']:
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
return [permission() for permission in permission_classes]
def get_queryset(self):
# Your existing logic is fine here
user = self.request.user
if user.user_type == "vendor":
# If the Vendor profile doesn't exist, create it
if not Vendor.objects.filter(user=user).exists():
return Vendor.objects.create(user=user)
return Vendor.objects.filter(user=user)
return Vendor.objects.all()
def get_object(self):
# Override get_object to ensure the user can only access their own Vendor profile
# when a specific ID is provided in the URL.
queryset = self.get_queryset()
obj = generics.get_object_or_404(queryset, user=self.request.user)
self.check_object_permissions(self.request, obj)
return obj
def update(self, request, *args, **kwargs):
# The update method will now handle the vendor profile correctly
# and ignore any user data in the payload.
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def increment_view_count(self, request, user__id=None):
vendor_obj = Vendor.objects.get(user__id=user__id)
vendor_obj.views += 1
vendor_obj.save()
UserViewModel.objects.create(
user_id=user__id
)
return Response({'views': vendor_obj.views}, status=status.HTTP_200_OK)
# Attorney ViewSet
class AttorneyViewSet(viewsets.ModelViewSet):
serializer_class = AttorneySerializer
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
# When creating an Attorney, link it to the currently authenticated user
# or handle user creation/association logic here.
# For demonstration, we'll assume request.user is the user to link.
# In a real app, you might have more complex logic (e.g., admin creating for another user).
serializer.save(user=self.request.user)
def get_queryset(self):
user = self.request.user
if user.user_type == "attorney":
if not Attorney.objects.filter(user=user).exists():
return Attorney.objects.create(user=user)
return Attorney.objects.filter(user=user)
else:
return Attorney.objects.all()
# Real Estate Agent ViewSet
class RealEstateAgentViewSet(viewsets.ModelViewSet):
serializer_class = RealEstateAgentSerializer
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
# Link to the currently authenticated user
serializer.save(user=self.request.user)
def get_queryset(self):
user = self.request.user
if user.user_type == "real_estate_agent":
if not RealEstateAgent.objects.filter(user=user).exists():
return RealEstateAgent.objects.create(user=user)
return RealEstateAgent.objects.filter(user=user)
else:
return RealEstateAgent.objects.all()
class 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.filter(owner__user=user)
return Property.objects.all() return Property.objects.all()
def perform_create(self, serializer): def perform_create(self, serializer):
if self.request.user.user_type == 'property_owner':
if self.request.user.user_type == "property_owner":
owner = PropertyOwner.objects.get(user=self.request.user) owner = PropertyOwner.objects.get(user=self.request.user)
## attempt to get the walkscore
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) serializer.save(owner=owner)
else: else:
serializer.save() 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): class VideoCategoryViewSet(viewsets.ModelViewSet):
queryset = VideoCategory.objects.all() queryset = VideoCategory.objects.all()
serializer_class = VideoCategorySerializer serializer_class = VideoCategorySerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
class VideoViewSet(viewsets.ModelViewSet): class VideoViewSet(viewsets.ModelViewSet):
queryset = Video.objects.all() queryset = Video.objects.all()
serializer_class = VideoSerializer serializer_class = VideoSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter] filter_backends = [DjangoFilterBackend, filters.SearchFilter]
search_fields = ['title', 'description'] search_fields = ["title", "description"]
filterset_fields = ['category'] filterset_fields = ["category"]
class UserVideoProgressViewSet(viewsets.ModelViewSet): class UserVideoProgressViewSet(viewsets.ModelViewSet):
queryset = UserVideoProgress.objects.all()
serializer_class = UserVideoProgressSerializer serializer_class = UserVideoProgressSerializer
permission_classes = [IsAuthenticated] 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) return UserVideoProgress.objects.filter(user=self.request.user)
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(user=self.request.user) serializer.save(user=self.request.user)
class ConversationViewSet(viewsets.ModelViewSet): class ConversationViewSet(viewsets.ModelViewSet):
serializer_class = ConversationSerializer
permission_classes = [IsAuthenticated, IsParticipant] 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): def get_queryset(self):
user = self.request.user user = self.request.user
if user.user_type == 'property_owner': if user.user_type == "property_owner":
owner = PropertyOwner.objects.get(user=user) owner = PropertyOwner.objects.get(user=user)
vendor_id = self.request.query_params.get("vendor")
if vendor_id:
return Conversation.objects.filter(
property_owner=owner, vendor=vendor_id
)
else:
return Conversation.objects.filter(property_owner=owner) return Conversation.objects.filter(property_owner=owner)
elif user.user_type == 'vendor': elif user.user_type == "vendor":
vendor = Vendor.objects.get(user=user) vendor = Vendor.objects.get(user=user)
return Conversation.objects.filter(vendor=vendor) return Conversation.objects.filter(vendor=vendor)
return Conversation.objects.none() return Conversation.objects.none()
def perform_create(self, serializer): def perform_create(self, serializer):
if self.request.user.user_type == 'property_owner': if self.request.user.user_type == "property_owner":
owner = PropertyOwner.objects.get(user=self.request.user) owner = PropertyOwner.objects.get(user=self.request.user)
serializer.save(property_owner=owner) 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) vendor = Vendor.objects.get(user=self.request.user)
serializer.save(vendor=vendor) serializer.save(vendor=vendor)
class MessageViewSet(viewsets.ModelViewSet): class MessageViewSet(viewsets.ModelViewSet):
serializer_class = MessageSerializer serializer_class = MessageSerializer
permission_classes = [IsAuthenticated, IsParticipant] permission_classes = [IsAuthenticated, IsParticipant]
def get_queryset(self): 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) conversation = get_object_or_404(Conversation, id=conversation_id)
self.check_object_permissions(self.request, conversation) 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): 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) conversation = get_object_or_404(Conversation, id=conversation_id)
self.check_object_permissions(self.request, conversation) self.check_object_permissions(self.request, conversation)
serializer.save(conversation=conversation, sender=self.request.user) serializer.save(conversation=conversation, sender=self.request.user)
def get_serializer_context(self): def get_serializer_context(self):
context = super().get_serializer_context() context = super().get_serializer_context()
context['conversation_id'] = self.kwargs.get('conversation_id') context["conversation_id"] = self.kwargs.get("conversation_id")
return context return context
class OfferViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsParticipantInOffer]
def get_serializer_class(self):
"""
Returns the serializer class to use depending on the action.
- For 'list' and 'retrieve' (read operations), use PropertyResponseSerializer.
- For 'create', 'update', 'partial_update' (write operations), use PropertyRequestSerializer.
"""
if self.action in ["list", "retrieve"]:
return OfferResponseSerializer
return OfferRequestSerializer
def get_queryset(self):
user_lookup = Q(user=self.request.user)
property_lookup = Q(property__owner__user=self.request.user)
null_previous_offer = Q(previous_offer=None)
return Offer.objects.filter(
(property_lookup & null_previous_offer)
| (user_lookup & null_previous_offer)
)
class BidViewSet(viewsets.ModelViewSet):
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 import os
from django.core.asgi import get_asgi_application 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.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
import core.routing import core.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') application = ProtocolTypeRouter(
{
application = ProtocolTypeRouter({
"http": get_asgi_application(), "http": get_asgi_application(),
"websocket": AuthMiddlewareStack( "websocket": AuthMiddlewareStack(
URLRouter( URLRouter(core.routing.websocket_urlpatterns)
core.routing.websocket_urlpatterns
)
), ),
}) }
)

View File

@@ -31,10 +31,21 @@ DEBUG = True
ALLOWED_HOSTS = ['*'] 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 # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"daphne",
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -54,6 +65,7 @@ INSTALLED_APPS = [
] ]
MIDDLEWARE = [ MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@@ -82,7 +94,7 @@ TEMPLATES = [
] ]
WSGI_APPLICATION = 'dta_service.wsgi.application' WSGI_APPLICATION = 'dta_service.wsgi.application'
ASGI_APPLICATION = 'config.asgi.application' ASGI_APPLICATION = 'dta_service.asgi.application'
# Database # Database
@@ -152,6 +164,10 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
} }
if DEBUG:
X_FRAME_OPTIONS = 'ALLOW-FROM 127.0.0.1:8010/'
# JWT Settings # JWT Settings
SIMPLE_JWT = { SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
@@ -168,8 +184,7 @@ SIMPLE_JWT = {
'JWK_URL': None, 'JWK_URL': None,
'LEEWAY': 0, 'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',), "AUTH_HEADER_TYPES": ("JWT",),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id', 'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id', 'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
@@ -210,3 +225,7 @@ SIMPLE_JWT = {
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 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

@@ -22,16 +22,10 @@ from rest_framework_simplejwt.views import (
from core.views import ( from core.views import (
CustomTokenObtainPairView, LogoutView, CustomTokenObtainPairView, LogoutView,
PasswordResetRequestView, PasswordResetConfirmView, PasswordResetRequestView, PasswordResetConfirmView,
UserRegisterView UserRegisterView, UserRetrieveView, UserSignTosView, PropertyDescriptionView
) )
from rest_framework.routers import DefaultRouter from django.conf import settings
from core.views import VideoCategoryViewSet, VideoViewSet, UserVideoProgressViewSet, VendorViewSet from django.conf.urls.static import static
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')
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@@ -45,9 +39,29 @@ urlpatterns = [
path('api/password-reset/confirm/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('api/password-reset/confirm/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
# API endpoints # API endpoints
path('api/user/', UserRetrieveView.as_view(), name='get_user'),
path('api/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/property-owners/', include('core.urls.property_owner')),
path('api/vendors/', include('core.urls.vendor')), path('api/vendors/', include('core.urls.vendor')),
path('api/properties/', include('core.urls.property')), path('api/properties/', include('core.urls.property')),
path('api/videos/', include('core.urls.video')), path('api/videos/', include('core.urls.video')),
path('api/conversations/', include('core.urls.conversation')), 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,234 +1,244 @@
from django.test import TestCase from django.test import TestCase
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from core.models import ( from core.models import (
PropertyOwner, Vendor, Property, VideoCategory, Video, PropertyOwner,
UserVideoProgress, Conversation, Message, PasswordResetToken Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
PasswordResetToken,
) )
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.utils import timezone from django.utils import timezone
User = get_user_model() User = get_user_model()
class UserModelTest(TestCase): class UserModelTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='test@example.com', email="test@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
def test_user_creation(self): def test_user_creation(self):
self.assertEqual(self.user.email, 'test@example.com') self.assertEqual(self.user.email, "test@example.com")
self.assertEqual(self.user.first_name, 'Test') self.assertEqual(self.user.first_name, "Test")
self.assertEqual(self.user.last_name, 'User') self.assertEqual(self.user.last_name, "User")
self.assertEqual(self.user.user_type, 'property_owner') self.assertEqual(self.user.user_type, "property_owner")
self.assertTrue(self.user.check_password('testpass123')) self.assertTrue(self.user.check_password("testpass123"))
def test_user_str(self): def test_user_str(self):
self.assertEqual(str(self.user), 'test@example.com') self.assertEqual(str(self.user), "test@example.com")
class PropertyOwnerModelTest(TestCase): class PropertyOwnerModelTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create( self.owner = PropertyOwner.objects.create(
user=self.user, user=self.user, phone_number="1234567890"
phone_number='1234567890'
) )
def test_property_owner_creation(self): def test_property_owner_creation(self):
self.assertEqual(self.owner.user, self.user) 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): def test_property_owner_str(self):
self.assertEqual(str(self.owner), 'Property Owner') self.assertEqual(str(self.owner), "Property Owner")
class VendorModelTest(TestCase): class VendorModelTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.user, user=self.user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor', business_type="contractor",
phone_number='1234567890', phone_number="1234567890",
address='123 Test St', address="123 Test St",
city='Testville', city="Testville",
state='TS', state="TS",
zip_code='12345' zip_code="12345",
) )
def test_vendor_creation(self): def test_vendor_creation(self):
self.assertEqual(self.vendor.user, self.user) self.assertEqual(self.vendor.user, self.user)
self.assertEqual(self.vendor.business_name, 'Test Vendor') self.assertEqual(self.vendor.business_name, "Test Vendor")
self.assertEqual(self.vendor.business_type, 'contractor') self.assertEqual(self.vendor.business_type, "contractor")
self.assertEqual(self.vendor.phone_number, '1234567890') self.assertEqual(self.vendor.phone_number, "1234567890")
self.assertEqual(self.vendor.address, '123 Test St') self.assertEqual(self.vendor.address, "123 Test St")
self.assertEqual(self.vendor.city, 'Testville') self.assertEqual(self.vendor.city, "Testville")
self.assertEqual(self.vendor.state, 'TS') self.assertEqual(self.vendor.state, "TS")
self.assertEqual(self.vendor.zip_code, '12345') self.assertEqual(self.vendor.zip_code, "12345")
def test_vendor_str(self): def test_vendor_str(self):
self.assertEqual(str(self.vendor), 'Test Vendor') self.assertEqual(str(self.vendor), "Test Vendor")
class PropertyModelTest(TestCase): class PropertyModelTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.user) self.owner = PropertyOwner.objects.create(user=self.user)
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00, market_value=500000.00,
loan_amount=400000.00, loan_amount=400000.00,
loan_interest_rate=3.5, loan_interest_rate=3.5,
loan_term=30, loan_term=30,
loan_start_date='2020-01-01' loan_start_date="2020-01-01",
) )
def test_property_creation(self): def test_property_creation(self):
self.assertEqual(self.property.owner, self.owner) self.assertEqual(self.property.owner, self.owner)
self.assertEqual(self.property.address, '123 Main St') self.assertEqual(self.property.address, "123 Main St")
self.assertEqual(self.property.city, 'Anytown') self.assertEqual(self.property.city, "Anytown")
self.assertEqual(self.property.state, 'CA') self.assertEqual(self.property.state, "CA")
self.assertEqual(self.property.zip_code, '90210') self.assertEqual(self.property.zip_code, "90210")
self.assertEqual(self.property.market_value, 500000.00) self.assertEqual(self.property.market_value, 500000.00)
self.assertEqual(self.property.loan_amount, 400000.00) self.assertEqual(self.property.loan_amount, 400000.00)
self.assertEqual(self.property.loan_interest_rate, 3.5) self.assertEqual(self.property.loan_interest_rate, 3.5)
self.assertEqual(self.property.loan_term, 30) 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): 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): class VideoModelTest(TestCase):
def setUp(self): def setUp(self):
self.category = VideoCategory.objects.create( self.category = VideoCategory.objects.create(
name='Test Category', name="Test Category", description="Test Description"
description='Test Description'
) )
self.video = Video.objects.create( self.video = Video.objects.create(
category=self.category, category=self.category,
title='Test Video', title="Test Video",
description='Test Video Description', description="Test Video Description",
link='https://example.com/video', link="https://example.com/video",
duration=300 duration=300,
) )
def test_video_creation(self): def test_video_creation(self):
self.assertEqual(self.video.category, self.category) self.assertEqual(self.video.category, self.category)
self.assertEqual(self.video.title, 'Test Video') self.assertEqual(self.video.title, "Test Video")
self.assertEqual(self.video.description, 'Test Video Description') self.assertEqual(self.video.description, "Test Video Description")
self.assertEqual(self.video.link, 'https://example.com/video') self.assertEqual(self.video.link, "https://example.com/video")
self.assertEqual(self.video.duration, 300) self.assertEqual(self.video.duration, 300)
def test_video_str(self): def test_video_str(self):
self.assertEqual(str(self.video), 'Test Video') self.assertEqual(str(self.video), "Test Video")
class UserVideoProgressModelTest(TestCase): class UserVideoProgressModelTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='user@example.com', email="user@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.category = VideoCategory.objects.create(name='Test Category') self.category = VideoCategory.objects.create(name="Test Category")
self.video = Video.objects.create( self.video = Video.objects.create(
category=self.category, category=self.category,
title='Test Video', title="Test Video",
link='https://example.com/video', link="https://example.com/video",
duration=300 duration=300,
) )
self.progress = UserVideoProgress.objects.create( self.progress = UserVideoProgress.objects.create(
user=self.user, user=self.user, video=self.video, progress=150
video=self.video,
progress=150
) )
def test_progress_creation(self): def test_progress_creation(self):
self.assertEqual(self.progress.user, self.user) self.assertEqual(self.progress.user, self.user)
self.assertEqual(self.progress.video, self.video) self.assertEqual(self.progress.video, self.video)
self.assertEqual(self.progress.progress, 150) 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): def test_status_update(self):
# Test not started # Test not started
self.progress.progress = 0 self.progress.progress = 0
self.progress.save() self.progress.save()
self.assertEqual(self.progress.status, 'not_started') self.assertEqual(self.progress.status, "not_started")
# Test completed # Test completed
self.progress.progress = 300 self.progress.progress = 300
self.progress.save() self.progress.save()
self.assertEqual(self.progress.status, 'completed') self.assertEqual(self.progress.status, "completed")
# Test in progress # Test in progress
self.progress.progress = 150 self.progress.progress = 150
self.progress.save() self.progress.save()
self.assertEqual(self.progress.status, 'in_progress') self.assertEqual(self.progress.status, "in_progress")
def test_progress_str(self): 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): class ConversationModelTest(TestCase):
def setUp(self): def setUp(self):
self.owner_user = User.objects.create_user( self.owner_user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.owner_user) self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user( self.vendor_user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.vendor_user, user=self.vendor_user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor' business_type="contractor",
) )
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
self.conversation = Conversation.objects.create( self.conversation = Conversation.objects.create(
property_owner=self.owner, property_owner=self.owner, vendor=self.vendor, property=self.property
vendor=self.vendor,
property=self.property
) )
def test_conversation_creation(self): def test_conversation_creation(self):
@@ -237,76 +247,75 @@ class ConversationModelTest(TestCase):
self.assertEqual(self.conversation.property, self.property) self.assertEqual(self.conversation.property, self.property)
def test_conversation_str(self): 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) self.assertEqual(str(self.conversation), expected_str)
class MessageModelTest(TestCase): class MessageModelTest(TestCase):
def setUp(self): def setUp(self):
self.owner_user = User.objects.create_user( self.owner_user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.owner_user) self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user( self.vendor_user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.vendor_user, user=self.vendor_user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor' business_type="contractor",
) )
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
self.conversation = Conversation.objects.create( self.conversation = Conversation.objects.create(
property_owner=self.owner, property_owner=self.owner, vendor=self.vendor, property=self.property
vendor=self.vendor,
property=self.property
) )
self.message = Message.objects.create( self.message = Message.objects.create(
conversation=self.conversation, conversation=self.conversation, sender=self.owner_user, text="Test message"
sender=self.owner_user,
text='Test message'
) )
def test_message_creation(self): def test_message_creation(self):
self.assertEqual(self.message.conversation, self.conversation) self.assertEqual(self.message.conversation, self.conversation)
self.assertEqual(self.message.sender, self.owner_user) 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) self.assertFalse(self.message.read)
def test_message_str(self): def test_message_str(self):
expected_str = f"Message from {self.owner_user} in {self.conversation}" expected_str = f"Message from {self.owner_user} in {self.conversation}"
self.assertEqual(str(self.message), expected_str) self.assertEqual(str(self.message), expected_str)
class PasswordResetTokenModelTest(TestCase): class PasswordResetTokenModelTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='user@example.com', email="user@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.token = PasswordResetToken.objects.create( self.token = PasswordResetToken.objects.create(
user=self.user, user=self.user, expires_at=timezone.now() + timedelta(hours=24)
expires_at=timezone.now() + timedelta(hours=24)
) )
def test_token_creation(self): def test_token_creation(self):

View File

@@ -2,236 +2,365 @@ from django.test import TestCase
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from core.serializers import ( from core.serializers import (
UserRegisterSerializer, PropertyOwnerSerializer, VendorSerializer, UserRegisterSerializer,
PropertySerializer, VideoSerializer, UserVideoProgressSerializer, PropertyOwnerSerializer,
ConversationSerializer, MessageSerializer, PasswordResetRequestSerializer, VendorSerializer,
PasswordResetConfirmSerializer PropertyRequestSerializer,
VideoSerializer,
UserVideoProgressSerializer,
ConversationRequestSerializer,
ConversationResponseSerializer,
MessageSerializer,
PasswordResetRequestSerializer,
PasswordResetConfirmSerializer,
OfferResponseSerializer,
OfferRequestSerializer,
) )
from core.models import ( from core.models import (
PropertyOwner, Vendor, Property, VideoCategory, Video, PropertyOwner,
UserVideoProgress, Conversation, Message, PasswordResetToken Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
PasswordResetToken,
Offer,
) )
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
User = get_user_model() User = get_user_model()
class UserRegisterSerializerTest(TestCase): class UserRegisterSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.valid_data = { self.valid_data = {
'email': 'test@example.com', "email": "test@example.com",
'first_name': 'Test', "first_name": "Test",
'last_name': 'User', "last_name": "User",
'user_type': 'property_owner', "user_type": "property_owner",
'password': 'testpass123', "password": "testpass123",
'password2': 'testpass123' "password2": "testpass123",
} }
def test_valid_serializer(self): def test_valid_serializer(self):
serializer = UserRegisterSerializer(data=self.valid_data) serializer = UserRegisterSerializer(data=self.valid_data)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
user = serializer.save() user = serializer.save()
self.assertEqual(user.email, 'test@example.com') self.assertEqual(user.email, "test@example.com")
self.assertEqual(user.first_name, 'Test') self.assertEqual(user.first_name, "Test")
self.assertEqual(user.last_name, 'User') self.assertEqual(user.last_name, "User")
self.assertEqual(user.user_type, 'property_owner') self.assertEqual(user.user_type, "property_owner")
def test_password_mismatch(self): def test_password_mismatch(self):
invalid_data = self.valid_data.copy() invalid_data = self.valid_data.copy()
invalid_data['password2'] = 'differentpass' invalid_data["password2"] = "differentpass"
serializer = UserRegisterSerializer(data=invalid_data) serializer = UserRegisterSerializer(data=invalid_data)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
class PropertyOwnerSerializerTest(TestCase): class PropertyOwnerSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.user_data = { self.user_data = {
'email': 'owner@example.com', "email": "owner@example.com",
'first_name': 'Property', "first_name": "Property",
'last_name': 'Owner', "last_name": "Owner",
'user_type': 'property_owner', "user_type": "property_owner",
'password': 'testpass123' "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): def test_create_property_owner(self):
serializer = PropertyOwnerSerializer(data=self.owner_data) serializer = PropertyOwnerSerializer(data=self.owner_data)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
owner = serializer.save() owner = serializer.save()
self.assertEqual(owner.user.email, 'owner@example.com') self.assertEqual(owner.user.email, "owner@example.com")
self.assertEqual(owner.phone_number, '1234567890') self.assertEqual(owner.phone_number, "1234567890")
def test_update_property_owner(self): def test_update_property_owner(self):
user = User.objects.create_user(**self.user_data) 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 = { update_data = {
'user': { "user": {
'first_name': 'NewName', "first_name": "NewName",
'last_name': 'NewLast', "last_name": "NewLast",
'email': 'newowner@example.com' "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()) self.assertTrue(serializer.is_valid())
updated_owner = serializer.save() updated_owner = serializer.save()
self.assertEqual(updated_owner.user.first_name, 'NewName') self.assertEqual(updated_owner.user.first_name, "NewName")
self.assertEqual(updated_owner.user.last_name, 'NewLast') self.assertEqual(updated_owner.user.last_name, "NewLast")
self.assertEqual(updated_owner.phone_number, '9876543210') self.assertEqual(updated_owner.phone_number, "9876543210")
class VendorSerializerTest(TestCase): class VendorSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.user_data = { self.user_data = {
'email': 'vendor@example.com', "email": "vendor@example.com",
'first_name': 'Vendor', "first_name": "Vendor",
'last_name': 'User', "last_name": "User",
'user_type': 'vendor', "user_type": "vendor",
'password': 'testpass123' "password": "testpass123",
} }
self.vendor_data = { self.vendor_data = {
'user': self.user_data, "user": self.user_data,
'business_name': 'Test Vendor', "business_name": "Test Vendor",
'business_type': 'contractor', "business_type": "electrician",
'phone_number': '1234567890', "phone_number": "1234567890",
'address': '123 Test St', "address": "123 Test St",
'city': 'Testville', "city": "Testville",
'state': 'TS', "state": "TS",
'zip_code': '12345' "zip_code": "12345",
} }
def test_create_vendor(self): def test_create_vendor(self):
serializer = VendorSerializer(data=self.vendor_data) serializer = VendorSerializer(data=self.vendor_data)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid(), serializer.errors)
vendor = serializer.save() vendor = serializer.save()
self.assertEqual(vendor.user.email, 'vendor@example.com') self.assertEqual(vendor.user.email, "vendor@example.com")
self.assertEqual(vendor.business_name, 'Test Vendor') self.assertEqual(vendor.business_name, "Test Vendor")
self.assertEqual(vendor.business_type, 'contractor') self.assertEqual(vendor.business_type, "electrician")
self.assertEqual(vendor.phone_number, '1234567890') self.assertEqual(vendor.phone_number, "1234567890")
self.assertEqual(vendor.address, '123 Test St') self.assertEqual(vendor.address, "123 Test St")
self.assertEqual(vendor.city, 'Testville') self.assertEqual(vendor.city, "Testville")
self.assertEqual(vendor.state, 'TS') self.assertEqual(vendor.state, "TS")
self.assertEqual(vendor.zip_code, '12345') self.assertEqual(vendor.zip_code, "12345")
def test_update_vendor(self): def test_update_vendor(self):
user = User.objects.create_user(**self.user_data) user = User.objects.create_user(**self.user_data)
vendor = Vendor.objects.create( vendor = Vendor.objects.create(
user=user, user=user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor', business_type="electrician",
phone_number='1234567890', phone_number="1234567890",
address='123 Test St', address="123 Test St",
city='Testville', city="Testville",
state='TS', state="TS",
zip_code='12345' zip_code="12345",
) )
update_data = { update_data = {
'user': { "user": {
'first_name': 'NewVendor', "first_name": "NewVendor",
'last_name': 'NewUser', "last_name": "NewUser",
'email': 'newvendor@example.com' "email": "newvendor@example.com",
}, },
'business_name': 'Updated Vendor', "business_name": "Updated Vendor",
'phone_number': '9876543210' "phone_number": "9876543210",
} }
serializer = VendorSerializer(instance=vendor, data=update_data, partial=True) serializer = VendorSerializer(instance=vendor, data=update_data, partial=True)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
updated_vendor = serializer.save() updated_vendor = serializer.save()
self.assertEqual(updated_vendor.user.first_name, 'NewVendor') self.assertEqual(updated_vendor.user.first_name, "NewVendor")
self.assertEqual(updated_vendor.user.last_name, 'NewUser') self.assertEqual(updated_vendor.user.last_name, "NewUser")
self.assertEqual(updated_vendor.business_name, 'Updated Vendor') self.assertEqual(updated_vendor.business_name, "Updated Vendor")
self.assertEqual(updated_vendor.phone_number, '9876543210') self.assertEqual(updated_vendor.phone_number, "9876543210")
class PropertySerializerTest(TestCase): class PropertySerializerTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.user) self.owner = PropertyOwner.objects.create(user=self.user)
self.property_data = { self.property_data = {
'owner': self.owner.pk, "owner": self.owner.pk,
'address': '123 Main St', "address": "123 Main St",
'city': 'Anytown', "city": "Anytown",
'state': 'CA', "state": "CA",
'zip_code': '90210', "zip_code": "90210",
'market_value': '500000.00', "market_value": "500000.00",
'loan_amount': '400000.00', "loan_amount": "400000.00",
'loan_interest_rate': '3.50', "loan_interest_rate": "3.50",
'loan_term': 30, "loan_term": 30,
'loan_start_date': '2020-01-01' "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): 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() property = serializer.save()
self.assertEqual(property.owner, self.owner) self.assertEqual(property.owner, self.owner)
self.assertEqual(property.address, '123 Main St') self.assertEqual(property.address, "123 Main St")
self.assertEqual(property.city, 'Anytown') self.assertEqual(property.city, "Anytown")
self.assertEqual(property.state, 'CA') self.assertEqual(property.state, "CA")
self.assertEqual(property.zip_code, '90210') self.assertEqual(property.zip_code, "90210")
self.assertEqual(str(property.market_value), '500000.00') self.assertEqual(str(property.market_value), "500000.00")
self.assertEqual(str(property.loan_amount), '400000.00') self.assertEqual(str(property.loan_amount), "400000.00")
self.assertEqual(str(property.loan_interest_rate), '3.50') self.assertEqual(str(property.loan_interest_rate), "3.50")
self.assertEqual(property.loan_term, 30) 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): def test_update_property(self):
property = Property.objects.create( property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
update_data = { update_data = {
'address': '456 New St', "address": "456 New St",
'city': 'Newtown', "city": "Newtown",
'state': 'NY', "state": "NY",
'zip_code': '10001', "zip_code": "10001",
'market_value': '600000.00' "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()) self.assertTrue(serializer.is_valid())
updated_property = serializer.save() updated_property = serializer.save()
self.assertEqual(updated_property.address, '456 New St') self.assertEqual(updated_property.address, "456 New St")
self.assertEqual(updated_property.city, 'Newtown') self.assertEqual(updated_property.city, "Newtown")
self.assertEqual(updated_property.state, 'NY') self.assertEqual(updated_property.state, "NY")
self.assertEqual(updated_property.zip_code, '10001') self.assertEqual(updated_property.zip_code, "10001")
self.assertEqual(str(updated_property.market_value), '600000.00') self.assertEqual(str(updated_property.market_value), "600000.00")
class VideoSerializerTest(TestCase): class VideoSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.category = VideoCategory.objects.create( self.category = VideoCategory.objects.create(
name='Test Category', name="Test Category", description="Test Description"
description='Test Description'
) )
self.video_data = { self.video_data = {
'category': { "category": {
'id': self.category.id, "id": self.category.id,
'name': self.category.name, "name": self.category.name,
'description': self.category.description "description": self.category.description,
}, },
'title': 'Test Video', "title": "Test Video",
'description': 'Test Video Description', "description": "Test Video Description",
'link': 'https://example.com/video', "link": "https://example.com/video",
'duration': 300 "duration": 300,
} }
def test_video_serializer(self): def test_video_serializer(self):
@@ -239,40 +368,38 @@ class VideoSerializerTest(TestCase):
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
video = serializer.save() video = serializer.save()
self.assertEqual(video.category, self.category) self.assertEqual(video.category, self.category)
self.assertEqual(video.title, 'Test Video') self.assertEqual(video.title, "Test Video")
self.assertEqual(video.description, 'Test Video Description') self.assertEqual(video.description, "Test Video Description")
self.assertEqual(video.link, 'https://example.com/video') self.assertEqual(video.link, "https://example.com/video")
self.assertEqual(video.duration, 300) self.assertEqual(video.duration, 300)
class UserVideoProgressSerializerTest(TestCase): class UserVideoProgressSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='user@example.com', email="user@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.category = VideoCategory.objects.create(name='Test Category') self.category = VideoCategory.objects.create(name="Test Category")
self.video = Video.objects.create( self.video = Video.objects.create(
category=self.category, category=self.category,
title='Test Video', title="Test Video",
link='https://example.com/video', link="https://example.com/video",
duration=300 duration=300,
) )
self.progress_data = { self.progress_data = {
'video': { "video": {
'id': self.video.id, "id": self.video.id,
'title': self.video.title, "title": self.video.title,
'link': self.video.link, "link": self.video.link,
'duration': self.video.duration, "duration": self.video.duration,
'category': { "category": {"id": self.category.id, "name": self.category.name},
'id': self.category.id,
'name': self.category.name
}
}, },
'user': self.user.pk, "user": self.user.pk,
'progress': 150 "progress": 150,
} }
def test_progress_serializer(self): def test_progress_serializer(self):
@@ -284,185 +411,191 @@ class UserVideoProgressSerializerTest(TestCase):
self.assertEqual(progress.video.title, self.video.title) self.assertEqual(progress.video.title, self.video.title)
self.assertEqual(progress.video.category.name, self.video.category.name) self.assertEqual(progress.video.category.name, self.video.category.name)
self.assertEqual(progress.progress, 150) self.assertEqual(progress.progress, 150)
self.assertEqual(progress.status, 'in_progress') self.assertEqual(progress.status, "in_progress")
def test_many_progress_serializer(self): def test_many_progress_serializer(self):
categories = [VideoCategory.objects.create(name='Category One'), categories = [
VideoCategory.objects.create(name='Category Two')] VideoCategory.objects.create(name="Category One"),
VideoCategory.objects.create(name="Category Two"),
]
videos = [ videos = [
Video.objects.create( Video.objects.create(
category=categories[0], category=categories[0],
title='Test Video 1', title="Test Video 1",
link='https://example.com/video1', link="https://example.com/video1",
duration=300 duration=300,
), ),
Video.objects.create( Video.objects.create(
category=categories[0], category=categories[0],
title='Test Video 2', title="Test Video 2",
link='https://example.com/video2', link="https://example.com/video2",
duration=300 duration=300,
), ),
Video.objects.create( Video.objects.create(
category=categories[0], category=categories[0],
title='Test Video 3', title="Test Video 3",
link='https://example.com/video3', link="https://example.com/video3",
duration=300 duration=300,
), ),
Video.objects.create( Video.objects.create(
category=categories[1], category=categories[1],
title='Test Video 4', title="Test Video 4",
link='https://example.com/video4', link="https://example.com/video4",
duration=300 duration=300,
) ),
] ]
progress_data = [ progress_data = [
UserVideoProgress.objects.create( UserVideoProgress.objects.create(
video=videos[0], video=videos[0], user=self.user, progress=100
user=self.user,
progress=100
), ),
UserVideoProgress.objects.create( UserVideoProgress.objects.create(
video=videos[1], video=videos[1], user=self.user, progress=30
user=self.user,
progress=30
), ),
UserVideoProgress.objects.create( UserVideoProgress.objects.create(
video=videos[2], video=videos[2], user=self.user, progress=0
user=self.user,
progress=0
), ),
UserVideoProgress.objects.create( UserVideoProgress.objects.create(
video=videos[3], video=videos[3], user=self.user, progress=0
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)) self.assertEqual(len(serializer.data), len(progress_data))
for i in range(len(serializer.data)): for i in range(len(serializer.data)):
self.assertEqual(serializer.data[i]['video']['title'], progress_data[i].video.title) self.assertEqual(
self.assertEqual(serializer.data[i]['video']['description'], progress_data[i].video.description) serializer.data[i]["video"]["title"], progress_data[i].video.title
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"]["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): class ConversationSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.owner_user = User.objects.create_user( self.owner_user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.owner_user) self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user( self.vendor_user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.vendor_user, user=self.vendor_user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor' business_type="contractor",
) )
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
self.conversation_data = { self.conversation_data = {
'property_owner': self.owner.pk, "property_owner": self.owner.pk,
'vendor': self.vendor.pk, "vendor": self.vendor.pk,
'property': self.property.pk "property": self.property.pk,
} }
def test_conversation_serializer(self): def test_conversation_serializer(self):
serializer = ConversationSerializer(data=self.conversation_data) serializer = ConversationRequestSerializer(data=self.conversation_data)
self.assertTrue(serializer.is_valid(), serializer.errors) self.assertTrue(serializer.is_valid(), serializer.errors)
conversation = serializer.save() conversation = serializer.save()
self.assertEqual(conversation.property_owner, self.owner) self.assertEqual(conversation.property_owner, self.owner)
self.assertEqual(conversation.vendor, self.vendor) self.assertEqual(conversation.vendor, self.vendor)
self.assertEqual(conversation.property, self.property) self.assertEqual(conversation.property, self.property)
class MessageSerializerTest(TestCase): class MessageSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.owner_user = User.objects.create_user( self.owner_user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.owner_user) self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.vendor_user = User.objects.create_user( self.vendor_user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.vendor_user, user=self.vendor_user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor' business_type="contractor",
) )
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
self.conversation = Conversation.objects.create( self.conversation = Conversation.objects.create(
property_owner=self.owner, property_owner=self.owner, vendor=self.vendor, property=self.property
vendor=self.vendor,
property=self.property
) )
self.message_data = { self.message_data = {"text": "Test message"}
'text': 'Test message'
}
def test_message_serializer(self): 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) serializer = MessageSerializer(data=context)
self.assertTrue(serializer.is_valid(), serializer.errors) self.assertTrue(serializer.is_valid(), serializer.errors)
message = serializer.save() message = serializer.save()
self.assertEqual(message.conversation, self.conversation) self.assertEqual(message.conversation, self.conversation)
self.assertEqual(message.sender, self.owner_user) self.assertEqual(message.sender, self.owner_user)
self.assertEqual(message.text, 'Test message') self.assertEqual(message.text, "Test message")
self.assertFalse(message.read) self.assertFalse(message.read)
class PasswordResetRequestSerializerTest(TestCase): class PasswordResetRequestSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='user@example.com', email="user@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.valid_data = {'email': 'user@example.com'} self.valid_data = {"email": "user@example.com"}
self.invalid_data = {'email': 'nonexistent@example.com'} self.invalid_data = {"email": "nonexistent@example.com"}
def test_valid_email(self): def test_valid_email(self):
serializer = PasswordResetRequestSerializer(data=self.valid_data) serializer = PasswordResetRequestSerializer(data=self.valid_data)
@@ -473,32 +606,33 @@ class PasswordResetRequestSerializerTest(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
class PasswordResetConfirmSerializerTest(TestCase): class PasswordResetConfirmSerializerTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='user@example.com', email="user@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.token = uuid.uuid4() self.token = uuid.uuid4()
self.valid_data = { self.valid_data = {
'token': self.token, "token": self.token,
'new_password': 'newpass123', "new_password": "newpass123",
'new_password2': 'newpass123' "new_password2": "newpass123",
} }
self.mismatch_data = { self.mismatch_data = {
'token': self.token, "token": self.token,
'new_password': 'newpass123', "new_password": "newpass123",
'new_password2': 'differentpass' "new_password2": "differentpass",
} }
def test_valid_password_reset(self): def test_valid_password_reset(self):
PasswordResetToken.objects.create( PasswordResetToken.objects.create(
user=self.user, user=self.user,
token=self.token, token=self.token,
expires_at=datetime.now() + timedelta(hours=24) expires_at=datetime.now() + timedelta(hours=24),
) )
serializer = PasswordResetConfirmSerializer(data=self.valid_data) serializer = PasswordResetConfirmSerializer(data=self.valid_data)
@@ -509,3 +643,69 @@ class PasswordResetConfirmSerializerTest(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
serializer.is_valid(raise_exception=True) 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)

View File

@@ -4,150 +4,147 @@ from rest_framework.test import APIClient
from rest_framework import status from rest_framework import status
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from core.models import ( from core.models import (
PropertyOwner, Vendor, Property, VideoCategory, Video, PropertyOwner,
UserVideoProgress, Conversation, Message, PasswordResetToken Vendor,
Property,
VideoCategory,
Video,
UserVideoProgress,
Conversation,
Message,
PasswordResetToken,
Offer,
) )
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.utils import timezone from django.utils import timezone
User = get_user_model() User = get_user_model()
class AuthenticationTests(TestCase): class AuthenticationTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='test@example.com', email="test@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.login_url = reverse('token_obtain_pair') self.login_url = reverse("token_obtain_pair")
self.refresh_url = reverse('token_refresh') self.refresh_url = reverse("token_refresh")
self.logout_url = reverse('logout') self.logout_url = reverse("logout")
self.register_url = reverse('register') self.register_url = reverse("register")
self.password_reset_url = reverse('password_reset') self.password_reset_url = reverse("password_reset")
self.password_reset_confirm_url = reverse('password_reset_confirm') self.password_reset_confirm_url = reverse("password_reset_confirm")
def test_user_login(self): def test_user_login(self):
data = { data = {"email": "test@example.com", "password": "testpass123"}
'email': 'test@example.com', response = self.client.post(self.login_url, data, format="json")
'password': 'testpass123'
}
response = self.client.post(self.login_url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('access', response.data) self.assertIn("access", response.data)
self.assertIn('refresh', response.data) self.assertIn("refresh", response.data)
self.assertIn('user', response.data) self.assertIn("user", response.data)
def test_user_login_invalid_credentials(self): def test_user_login_invalid_credentials(self):
data = { data = {"email": "test@example.com", "password": "wrongpassword"}
'email': 'test@example.com', response = self.client.post(self.login_url, data, format="json")
'password': 'wrongpassword'
}
response = self.client.post(self.login_url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_token_refresh(self): def test_token_refresh(self):
# First login to get refresh token # First login to get refresh token
login_data = { login_data = {"email": "test@example.com", "password": "testpass123"}
'email': 'test@example.com', login_response = self.client.post(self.login_url, login_data, format="json")
'password': 'testpass123' refresh_token = login_response.data["refresh"]
}
login_response = self.client.post(self.login_url, login_data, format='json')
refresh_token = login_response.data['refresh']
# Now refresh the token # Now refresh the token
refresh_data = { refresh_data = {"refresh": refresh_token}
'refresh': refresh_token response = self.client.post(self.refresh_url, refresh_data, format="json")
}
response = self.client.post(self.refresh_url, refresh_data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('access', response.data) self.assertIn("access", response.data)
def test_user_logout(self): def test_user_logout(self):
# First login to get tokens # First login to get tokens
login_data = { login_data = {"email": "test@example.com", "password": "testpass123"}
'email': 'test@example.com', login_response = self.client.post(self.login_url, login_data, format="json")
'password': 'testpass123' self.assertEqual(
} login_response.status_code, status.HTTP_200_OK, login_response.text
login_response = self.client.post(self.login_url, login_data, format='json') )
self.assertEqual(login_response.status_code, status.HTTP_200_OK, login_response.text)
refresh_token = login_response.data['refresh'] refresh_token = login_response.data["refresh"]
# Now logout # Now logout
logout_data = { logout_data = {"refresh_token": refresh_token}
'refresh_token': refresh_token response = self.client.post(self.logout_url, logout_data, format="json")
} self.assertEqual(
response = self.client.post(self.logout_url, logout_data, format='json') response.status_code, status.HTTP_205_RESET_CONTENT, response.text
self.assertEqual(response.status_code, status.HTTP_205_RESET_CONTENT, response.text) )
def test_user_registration(self): def test_user_registration(self):
data = { data = {
'email': 'newuser@example.com', "email": "newuser@example.com",
'first_name': 'New', "first_name": "New",
'last_name': 'User', "last_name": "User",
'user_type': 'vendor', "user_type": "vendor",
'password': 'newpass123', "password": "newpass123",
'password2': 'newpass123' "password2": "newpass123",
} }
response = self.client.post(self.register_url, data, format='json') response = self.client.post(self.register_url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(User.objects.filter(email='newuser@example.com').exists()) self.assertTrue(User.objects.filter(email="newuser@example.com").exists())
def test_password_reset_request(self): def test_password_reset_request(self):
data = { data = {"email": "test@example.com"}
'email': 'test@example.com' response = self.client.post(self.password_reset_url, data, format="json")
}
response = self.client.post(self.password_reset_url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(PasswordResetToken.objects.filter(user=self.user).exists()) self.assertTrue(PasswordResetToken.objects.filter(user=self.user).exists())
def test_password_reset_confirm(self): def test_password_reset_confirm(self):
# First create a reset token # First create a reset token
expires_at = timezone.now() + timedelta(hours=24) expires_at = timezone.now() + timedelta(hours=24)
token = PasswordResetToken.objects.create( token = PasswordResetToken.objects.create(user=self.user, expires_at=expires_at)
user=self.user,
expires_at=expires_at
)
data = { data = {
'token': str(token.token), "token": str(token.token),
'new_password': 'newpass123', "new_password": "newpass123",
'new_password2': 'newpass123' "new_password2": "newpass123",
} }
response = self.client.post(self.password_reset_confirm_url, data, format='json') response = self.client.post(
self.password_reset_confirm_url, data, format="json"
)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
# Verify password was changed # Verify password was changed
self.user.refresh_from_db() self.user.refresh_from_db()
self.assertTrue(self.user.check_password('newpass123')) self.assertTrue(self.user.check_password("newpass123"))
# Verify token was marked as used # Verify token was marked as used
token.refresh_from_db() token.refresh_from_db()
self.assertTrue(token.used) self.assertTrue(token.used)
class PropertyOwnerViewTests(TestCase): class PropertyOwnerViewTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
)
self.owner = PropertyOwner.objects.create(
user=self.user, phone_number="1234567890"
) )
self.owner = PropertyOwner.objects.create(user=self.user, phone_number='1234567890')
self.other_user = User.objects.create_user( self.other_user = User.objects.create_user(
email='other@example.com', email="other@example.com",
first_name='Other', first_name="Other",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.other_owner = PropertyOwner.objects.create(user=self.other_user) self.other_owner = PropertyOwner.objects.create(user=self.other_user)
self.url = reverse('property-owner-list') self.url = reverse("property-owner-list")
# Authenticate # Authenticate
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
@@ -159,57 +156,60 @@ class PropertyOwnerViewTests(TestCase):
def test_create_property_owner(self): def test_create_property_owner(self):
data = { data = {
'user': { "user": {
'email': 'newowner@example.com', "email": "newowner@example.com",
'first_name': 'New', "first_name": "New",
'last_name': 'Owner', "last_name": "Owner",
'user_type': 'property_owner', "user_type": "property_owner",
'password': 'testpass123' "password": "testpass123",
}, },
'phone_number': '9876543210' "phone_number": "9876543210",
} }
response = self.client.post(self.url, data, format='json') response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
self.assertTrue(PropertyOwner.objects.filter(user__email='newowner@example.com').exists()) self.assertTrue(
PropertyOwner.objects.filter(user__email="newowner@example.com").exists()
)
def test_update_property_owner(self): def test_update_property_owner(self):
url = f"{self.url}{self.owner.pk}/" url = f"{self.url}{self.owner.pk}/"
data = { data = {
'user': { "user": {
'first_name': 'Updated', "first_name": "Updated",
'last_name': 'Owner', "last_name": "Owner",
'email': 'new_email@email.com', "email": "new_email@email.com",
'user_type': 'property_owner' "user_type": "property_owner",
}, },
'phone_number': '9999999999' "phone_number": "9999999999",
} }
response = self.client.put(url, data, format='json') response = self.client.put(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) self.assertEqual(response.status_code, status.HTTP_200_OK, response.text)
self.owner.refresh_from_db() self.owner.refresh_from_db()
self.assertEqual(self.owner.user.first_name, 'Updated') self.assertEqual(self.owner.user.first_name, "Updated")
self.assertEqual(self.owner.phone_number, '9999999999') self.assertEqual(self.owner.phone_number, "9999999999")
class VendorViewTests(TestCase): class VendorViewTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.user, user=self.user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor', business_type="contractor",
phone_number='1234567890', phone_number="1234567890",
address='123 Test St', address="123 Test St",
city='Testville', city="Testville",
state='TS', state="TS",
zip_code='12345' zip_code="12345",
) )
self.url = reverse('vendor-list') self.url = reverse("vendor-list")
# Authenticate # Authenticate
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
@@ -221,62 +221,63 @@ class VendorViewTests(TestCase):
def test_create_vendor(self): def test_create_vendor(self):
data = { data = {
'user': { "user": {
'email': 'newvendor@example.com', "email": "newvendor@example.com",
'first_name': 'New', "first_name": "New",
'last_name': 'Vendor', "last_name": "Vendor",
'user_type': 'vendor', "user_type": "vendor",
'password': 'testpass123' "password": "testpass123",
}, },
'business_name': 'New Vendor', "business_name": "New Vendor",
'business_type': 'inspector', "business_type": "inspector",
'phone_number': '9876543210', "phone_number": "9876543210",
'address': '456 New St', "address": "456 New St",
'city': 'Newtown', "city": "Newtown",
'state': 'NS', "state": "NS",
'zip_code': '54321' "zip_code": "54321",
} }
response = self.client.post(self.url, data, format='json') response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(Vendor.objects.filter(business_name='New Vendor').exists()) self.assertTrue(Vendor.objects.filter(business_name="New Vendor").exists())
def test_update_vendor(self): def test_update_vendor(self):
url = f"{self.url}{self.vendor.pk}/" url = f"{self.url}{self.vendor.pk}/"
data = { data = {
'user': { "user": {
'first_name': 'Updated', "first_name": "Updated",
'last_name': 'Vendor', "last_name": "Vendor",
}, },
'business_name': 'Updated Vendor', "business_name": "Updated Vendor",
'phone_number': '9999999999' "phone_number": "9999999999",
} }
response = self.client.patch(url, data, format='json') response = self.client.patch(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) self.assertEqual(response.status_code, status.HTTP_200_OK, response.text)
self.vendor.refresh_from_db() self.vendor.refresh_from_db()
self.assertEqual(self.vendor.user.first_name, 'Updated') self.assertEqual(self.vendor.user.first_name, "Updated")
self.assertEqual(self.vendor.business_name, 'Updated Vendor') self.assertEqual(self.vendor.business_name, "Updated Vendor")
self.assertEqual(self.vendor.phone_number, '9999999999') self.assertEqual(self.vendor.phone_number, "9999999999")
class PropertyViewTests(TestCase): class PropertyViewTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.owner_user = User.objects.create_user( self.owner_user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.owner_user) self.owner = PropertyOwner.objects.create(user=self.owner_user)
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
self.url = reverse('property-list') self.url = reverse("property-list")
# Authenticate as property owner # Authenticate as property owner
self.client.force_authenticate(user=self.owner_user) self.client.force_authenticate(user=self.owner_user)
@@ -288,51 +289,48 @@ class PropertyViewTests(TestCase):
def test_create_property(self): def test_create_property(self):
data = { data = {
'owner': self.owner.pk, "owner": self.owner.pk,
'address': '456 New St', "address": "456 New St",
'city': 'Newtown', "city": "Newtown",
'state': 'NY', "state": "NY",
'zip_code': '10001', "zip_code": "10001",
'market_value': '600000.00' "market_value": "600000.00",
} }
response = self.client.post(self.url, data, format='json') response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
self.assertTrue(Property.objects.filter(address='456 New St').exists()) self.assertTrue(Property.objects.filter(address="456 New St").exists())
def test_update_property(self): def test_update_property(self):
url = f"{self.url}{self.property.pk}/" url = f"{self.url}{self.property.pk}/"
data = { data = {"address": "789 Updated St", "market_value": "550000.00"}
'address': '789 Updated St', response = self.client.patch(url, data, format="json")
'market_value': '550000.00'
}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.property.refresh_from_db() self.property.refresh_from_db()
self.assertEqual(self.property.address, '789 Updated St') self.assertEqual(self.property.address, "789 Updated St")
self.assertEqual(str(self.property.market_value), '550000.00') self.assertEqual(str(self.property.market_value), "550000.00")
class VideoViewTests(TestCase): class VideoViewTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='user@example.com', email="user@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.category = VideoCategory.objects.create( self.category = VideoCategory.objects.create(
name='Test Category', name="Test Category", description="Test Description"
description='Test Description'
) )
self.video = Video.objects.create( self.video = Video.objects.create(
category=self.category, category=self.category,
title='Test Video', title="Test Video",
description='Test Video Description', description="Test Video Description",
link='https://example.com/video', link="https://example.com/video",
duration=300 duration=300,
) )
self.url = reverse('video-list') self.url = reverse("video-list")
# Authenticate # Authenticate
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
@@ -344,97 +342,83 @@ class VideoViewTests(TestCase):
def test_create_video(self): def test_create_video(self):
data = { data = {
'category': { "category": {"name": "New Category", "description": "a description"},
"name": "New Category", "title": "New Video",
"description": "a description" "description": "New Video Description",
}, "link": "https://example.com/new-video",
'title': 'New Video', "duration": 240,
'description': 'New Video Description',
'link': 'https://example.com/new-video',
'duration': 240
} }
response = self.client.post(self.url, data, format='json') response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
self.assertTrue(Video.objects.filter(title='New Video').exists()) self.assertTrue(Video.objects.filter(title="New Video").exists())
def test_update_video(self): def test_update_video(self):
url = f"{self.url}{self.video.pk}/" url = f"{self.url}{self.video.pk}/"
data = { data = {"title": "Updated Video", "duration": 360}
'title': 'Updated Video', response = self.client.patch(url, data, format="json")
'duration': 360
}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) self.assertEqual(response.status_code, status.HTTP_200_OK, response.text)
self.video.refresh_from_db() self.video.refresh_from_db()
self.assertEqual(self.video.title, 'Updated Video') self.assertEqual(self.video.title, "Updated Video")
self.assertEqual(self.video.duration, 360) self.assertEqual(self.video.duration, 360)
class UserVideoProgressViewTests(TestCase): class UserVideoProgressViewTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.user = User.objects.create_user( self.user = User.objects.create_user(
email='user@example.com', email="user@example.com",
first_name='Test', first_name="Test",
last_name='User', last_name="User",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.category = VideoCategory.objects.create(name='Test Category') self.category = VideoCategory.objects.create(name="Test Category")
self.video = Video.objects.create( self.video = Video.objects.create(
category=self.category, category=self.category,
title='Test Video', title="Test Video",
link='https://example.com/video', link="https://example.com/video",
duration=300 duration=300,
) )
self.video_1 = Video.objects.create( self.video_1 = Video.objects.create(
category=self.category, category=self.category,
title='Test Video 1', title="Test Video 1",
link='https://example.com/video_1', link="https://example.com/video_1",
duration=300 duration=300,
) )
self.video_2 = Video.objects.create( self.video_2 = Video.objects.create(
category=self.category, category=self.category,
title='Test Video 2', title="Test Video 2",
link='https://example.com/video_2', link="https://example.com/video_2",
duration=300 duration=300,
) )
self.progress = UserVideoProgress.objects.create( self.progress = UserVideoProgress.objects.create(
user=self.user, user=self.user, video=self.video, progress=150
video=self.video,
progress=150
) )
UserVideoProgress.objects.create( UserVideoProgress.objects.create(
user=self.user, user=self.user, video=self.video_1, progress=150
video=self.video_1,
progress=150
) )
UserVideoProgress.objects.create( UserVideoProgress.objects.create(
user=self.user, user=self.user, video=self.video_2, progress=150
video=self.video_2,
progress=150
) )
self.url = reverse('video-progress-list') self.url = reverse("video-progress-list")
# Authenticate # Authenticate
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
def test_get_video_progress(self): def test_get_video_progress(self):
response = self.client.get(f"{self.url}", kwargs={'format','json'}) response = self.client.get(self.url)
breakpoint()
self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) self.assertEqual(response.status_code, status.HTTP_200_OK, response.text)
breakpoint() self.assertEqual(len(response.data), 3)
self.assertEqual(len(response.data), 1)
def test_update_video_progress(self): def test_update_video_progress(self):
url = f"{self.url}{self.progress.pk}/" url = f"{self.url}{self.progress.pk}/"
data = { data = {"progress": 200}
'progress': 200 response = self.client.patch(url, data, format="json")
}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.progress.refresh_from_db() self.progress.refresh_from_db()
self.assertEqual(self.progress.progress, 200) self.assertEqual(self.progress.progress, 200)
self.assertEqual(self.progress.status, 'in_progress') self.assertEqual(self.progress.status, "in_progress")
class ConversationViewTests(TestCase): class ConversationViewTests(TestCase):
def setUp(self): def setUp(self):
@@ -442,46 +426,44 @@ class ConversationViewTests(TestCase):
# Create owner user # Create owner user
self.owner_user = User.objects.create_user( self.owner_user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.owner_user) self.owner = PropertyOwner.objects.create(user=self.owner_user)
# Create vendor user # Create vendor user
self.vendor_user = User.objects.create_user( self.vendor_user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.vendor_user, user=self.vendor_user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor' business_type="contractor",
) )
# Create property # Create property
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
# Create conversation # Create conversation
self.conversation = Conversation.objects.create( self.conversation = Conversation.objects.create(
property_owner=self.owner, property_owner=self.owner, vendor=self.vendor, property=self.property
vendor=self.vendor,
property=self.property
) )
self.url = reverse('conversation-list') self.url = reverse("conversation-list")
# Authenticate as owner # Authenticate as owner
self.client.force_authenticate(user=self.owner_user) self.client.force_authenticate(user=self.owner_user)
@@ -493,13 +475,27 @@ class ConversationViewTests(TestCase):
def test_create_conversation(self): def test_create_conversation(self):
data = { data = {
'property_owner': self.owner.pk, "property_owner": self.owner.pk,
'vendor': self.vendor.pk, "vendor": self.vendor.pk,
'property': self.property.pk "property": self.property.pk,
} }
response = self.client.post(self.url, data, format='json') response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
self.assertTrue(Conversation.objects.filter(property_owner=self.owner, vendor=self.vendor, property=self.property).exists()) self.assertTrue(
Conversation.objects.filter(
property_owner=self.owner, vendor=self.vendor, property=self.property
).exists()
)
def test_create_conversation_without_property(self):
data = {"property_owner": self.owner.pk, "vendor": self.vendor.pk}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
self.assertTrue(
Conversation.objects.filter(
property_owner=self.owner, vendor=self.vendor, property=self.property
).exists()
)
def test_get_conversations_as_vendor(self): def test_get_conversations_as_vendor(self):
# Switch to vendor user # Switch to vendor user
@@ -508,59 +504,58 @@ class ConversationViewTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
class MessageViewTests(TestCase): class MessageViewTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
# Create owner user # Create owner user
self.owner_user = User.objects.create_user( self.owner_user = User.objects.create_user(
email='owner@example.com', email="owner@example.com",
first_name='Property', first_name="Property",
last_name='Owner', last_name="Owner",
user_type='property_owner', user_type="property_owner",
password='testpass123' password="testpass123",
) )
self.owner = PropertyOwner.objects.create(user=self.owner_user) self.owner = PropertyOwner.objects.create(user=self.owner_user)
# Create vendor user # Create vendor user
self.vendor_user = User.objects.create_user( self.vendor_user = User.objects.create_user(
email='vendor@example.com', email="vendor@example.com",
first_name='Vendor', first_name="Vendor",
last_name='User', last_name="User",
user_type='vendor', user_type="vendor",
password='testpass123' password="testpass123",
) )
self.vendor = Vendor.objects.create( self.vendor = Vendor.objects.create(
user=self.vendor_user, user=self.vendor_user,
business_name='Test Vendor', business_name="Test Vendor",
business_type='contractor' business_type="contractor",
) )
# Create property # Create property
self.property = Property.objects.create( self.property = Property.objects.create(
owner=self.owner, owner=self.owner,
address='123 Main St', address="123 Main St",
city='Anytown', city="Anytown",
state='CA', state="CA",
zip_code='90210', zip_code="90210",
market_value=500000.00 market_value=500000.00,
) )
# Create conversation # Create conversation
self.conversation = Conversation.objects.create( self.conversation = Conversation.objects.create(
property_owner=self.owner, property_owner=self.owner, vendor=self.vendor, property=self.property
vendor=self.vendor,
property=self.property
) )
# Create message # Create message
self.message = Message.objects.create( self.message = Message.objects.create(
conversation=self.conversation, conversation=self.conversation, sender=self.owner_user, text="Test message"
sender=self.owner_user,
text='Test message'
) )
self.url = reverse('conversation-messages', kwargs={'conversation_id': self.conversation.pk}) self.url = reverse(
"conversation-messages", kwargs={"conversation_id": self.conversation.pk}
)
# Authenticate as owner # Authenticate as owner
self.client.force_authenticate(user=self.owner_user) self.client.force_authenticate(user=self.owner_user)
@@ -572,13 +567,13 @@ class MessageViewTests(TestCase):
def test_create_message(self): def test_create_message(self):
data = { data = {
'sender': self.owner.pk, "sender": self.owner.pk,
'text': 'New message', "text": "New message",
'conversation': self.conversation.pk "conversation": self.conversation.pk,
} }
response = self.client.post(self.url, data, format='json') response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
self.assertTrue(Message.objects.filter(text='New message').exists()) self.assertTrue(Message.objects.filter(text="New message").exists())
def test_get_messages_as_vendor(self): def test_get_messages_as_vendor(self):
# Switch to vendor user # Switch to vendor user
@@ -586,3 +581,136 @@ class MessageViewTests(TestCase):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
class OfferViewTests(TestCase):
def setUp(self):
self.client = APIClient()
# Create owner user
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)
# Create property
self.property = Property.objects.create(
owner=self.owner,
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
)
self.property_1 = Property.objects.create(
owner=self.owner,
address="654 Main Cir",
city="Something",
state="OH",
zip_code="44512",
market_value=100000.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)
# Create property
self.property_2 = Property.objects.create(
owner=self.owner_2,
address="123 Main St",
city="Anytown",
state="CA",
zip_code="90210",
market_value=500000.00,
)
self.url = reverse("offer-list")
self.offer_user = User.objects.create_user(
email="offer@example.com",
first_name="Offer",
last_name="Owner",
user_type="property_owner",
password="testpass123",
)
self.offer_1 = Offer.objects.create(
user=self.offer_user, property=self.property
)
self.offer_2 = Offer.objects.create(
user=self.owner_user, property=self.property, previous_offer=self.offer_1
)
self.offer_3 = Offer.objects.create(
user=self.offer_user, property=self.property_2
)
self.offer_4 = Offer.objects.create(
user=self.offer_user, property=self.property_1
)
self.offer_5 = Offer.objects.create(
user=self.owner_user, property=self.property_2
)
# Authenticate as owner
self.client.force_authenticate(user=self.owner_user)
def test_get_offers(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# we aren't going to show the last one
self.assertEqual(len(response.data), 3)
def test_create_offer(self):
self.client.force_authenticate(user=self.owner_user_2)
data = {"user": self.owner_user.pk, "property": self.property_1.pk}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
self.assertEqual(response.data["status"], "draft")
self.client.force_authenticate(user=self.owner_user_2)
data = {"user": self.owner_user_2.pk, "property": self.property_1.pk}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text)
def test_submit_offer(self):
self.client.force_authenticate(user=self.owner_user_2)
data = {
"user": self.owner_user.pk,
"property": self.property_2.pk,
"status": "submitted",
}
url = f"{self.url}{self.offer_5.pk}/"
response = self.client.put(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK, response.text)
self.assertEqual(response.data["status"], "submitted")
def test_accept_offer(self):
self.client.force_authenticate(user=self.owner_user_2)
data = {
"user": self.owner_user.pk,
"property": self.property_2.pk,
"status": "accepted",
}
url = f"{self.url}{self.offer_5.pk}/"
response = self.client.put(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK, response.text)
self.assertEqual(response.data["status"], "accepted")
def test_accept_offer(self):
self.client.force_authenticate(user=self.owner_user_2)
data = {
"user": self.owner_user.pk,
"property": self.property_2.pk,
"status": "rejected",
}
url = f"{self.url}{self.offer_5.pk}/"
response = self.client.put(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK, response.text)
self.assertEqual(response.data["status"], "rejected")
# def test_counter_offer(self):
# raise NotImplementedError

View File

@@ -1,22 +1,72 @@
annotated-types==0.7.0
anyio==4.9.0
asgiref==3.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 cfgv==3.4.0
channels==4.2.2 channels==4.2.2
channels_redis==4.2.1 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 distlib==0.3.9
Django==5.2.4 Django==5.2.4
django-cors-headers==4.7.0
django-filter==25.1 django-filter==25.1
djangorestframework==3.16.0 djangorestframework==3.16.0
djangorestframework_simplejwt==5.5.0 djangorestframework_simplejwt==5.5.0
filelock==3.18.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 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 msgpack==1.1.1
mypy_extensions==1.1.0
nodeenv==1.9.1 nodeenv==1.9.1
ollama==0.5.1
orjson==3.11.1
packaging==25.0
pathspec==0.12.1
pillow==11.3.0 pillow==11.3.0
platformdirs==4.3.8 platformdirs==4.3.8
pre_commit==4.2.0 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 PyJWT==2.9.0
pyOpenSSL==25.1.0
python-decouple==3.8 python-decouple==3.8
PyYAML==6.0.2 PyYAML==6.0.2
redis==6.2.0 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 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 virtualenv==20.31.2
zope.interface==7.2
zstandard==0.23.0