big update
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -257,3 +257,4 @@ local_settings.py
|
||||
|
||||
.env
|
||||
db.sqlite3
|
||||
media/
|
||||
|
||||
@@ -1,3 +1,210 @@
|
||||
from django.contrib import admin
|
||||
from core.models import (
|
||||
User,
|
||||
PropertyOwner,
|
||||
Message,
|
||||
Vendor,
|
||||
Property,
|
||||
VideoCategory,
|
||||
Video,
|
||||
UserVideoProgress,
|
||||
Conversation,
|
||||
Offer,
|
||||
PropertyPictures,
|
||||
OpenHouse,
|
||||
PropertySaleInfo,
|
||||
PropertyTaxInfo,
|
||||
PropertyWalkScoreInfo,
|
||||
SchoolInfo,
|
||||
Attorney, RealEstateAgent, UserViewModel, PropertySave
|
||||
)
|
||||
|
||||
|
||||
# Register your models here.
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
model = User
|
||||
list_display = (
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"has_usable_password",
|
||||
"user_type",
|
||||
# "has_signed_tos",
|
||||
"last_login",
|
||||
)
|
||||
search_fields = ("fields", "email", "first_name", "last_name", "user_type")
|
||||
|
||||
|
||||
class PropertyOwnerAdmin(admin.ModelAdmin):
|
||||
model = PropertyOwner
|
||||
list_display = (
|
||||
"pk",
|
||||
"user",
|
||||
"phone_number",
|
||||
)
|
||||
|
||||
|
||||
class VendorAdmin(admin.ModelAdmin):
|
||||
model = Vendor
|
||||
list_display = ("business_name", "business_type", "address")
|
||||
search_fields = (
|
||||
"business_name",
|
||||
"business_type",
|
||||
"phone_number",
|
||||
"state",
|
||||
"zip_code",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class VideoCategoryAdmin(admin.ModelAdmin):
|
||||
model = VideoCategory
|
||||
list_display = ("name", "description")
|
||||
search_fields = ("name",)
|
||||
|
||||
|
||||
class VideoAdmin(admin.ModelAdmin):
|
||||
model = Video
|
||||
list_display = ("title", "description", "category__name", "duration", "link")
|
||||
search_fields = ("name",)
|
||||
|
||||
|
||||
class UserVideoProgressAdmin(admin.ModelAdmin):
|
||||
model = UserVideoProgress
|
||||
|
||||
|
||||
class MessageStackedInline(admin.StackedInline):
|
||||
model = Message
|
||||
extra = 1
|
||||
|
||||
|
||||
class ConversationAdmin(admin.ModelAdmin):
|
||||
model = Conversation
|
||||
inlines = [MessageStackedInline]
|
||||
|
||||
|
||||
class OfferAdmin(admin.ModelAdmin):
|
||||
model = Offer
|
||||
list_display = ("pk", "user", "property", "status")
|
||||
search_fields = ("user", "status")
|
||||
|
||||
|
||||
class PropertyPicturesAdmin(admin.ModelAdmin):
|
||||
model = PropertyPictures
|
||||
list_display = ("id", "image")
|
||||
|
||||
|
||||
class OpenHouseAdmin(admin.ModelAdmin):
|
||||
model = OpenHouse
|
||||
|
||||
|
||||
class PropertySaleInfoAdmin(admin.ModelAdmin):
|
||||
model = PropertySaleInfo
|
||||
|
||||
|
||||
class PropertyTaxInfoAdmin(admin.ModelAdmin):
|
||||
model = PropertyTaxInfo
|
||||
|
||||
|
||||
class PropertyWalkScoreInfoAdmin(admin.ModelAdmin):
|
||||
model = PropertyWalkScoreInfo
|
||||
|
||||
|
||||
class SchoolInfoAdmin(admin.ModelAdmin):
|
||||
model = SchoolInfo
|
||||
|
||||
class PropertyWalkScoreInfoStackedInline(admin.StackedInline):
|
||||
model = PropertyWalkScoreInfo
|
||||
|
||||
class SchoolInfoStackedInline(admin.StackedInline):
|
||||
model = SchoolInfo
|
||||
|
||||
class PropertyAdmin(admin.ModelAdmin):
|
||||
model = Property
|
||||
list_display = ("pk", "owner", "address", "city", "state", "zip_code")
|
||||
search_fields = ("address", "city", "state", "zip_code", "owner")
|
||||
inlines = [PropertyWalkScoreInfoStackedInline, SchoolInfoStackedInline]
|
||||
|
||||
|
||||
# Registering the new Attorney model
|
||||
@admin.register(Attorney)
|
||||
class AttorneyAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'firm_name', 'bar_number', 'phone_number', 'city', 'state', 'years_experience')
|
||||
list_filter = ('state', 'years_experience', 'specialties') # You might need custom list_filter for JSONField
|
||||
search_fields = ('user__email', 'user__first_name', 'user__last_name', 'firm_name', 'bar_number', 'city')
|
||||
# Use filter_horizontal for JSONField if you want a nice interface for many-to-many like selection
|
||||
# filter_horizontal = ('specialties', 'licensed_states')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('user', 'firm_name', 'bar_number', 'phone_number', 'address', 'city', 'state', 'zip_code', 'bio', 'profile_picture', 'website')
|
||||
}),
|
||||
('Professional Details', {
|
||||
'fields': ('specialties', 'years_experience', 'licensed_states')
|
||||
}),
|
||||
('Location', {
|
||||
'fields': ('latitude', 'longitude'),
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
|
||||
|
||||
# Registering the new RealEstateAgent model
|
||||
@admin.register(RealEstateAgent)
|
||||
class RealEstateAgentAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'brokerage_name', 'license_number', 'phone_number', 'city', 'state', 'agent_type', 'years_experience')
|
||||
list_filter = ('agent_type', 'state', 'years_experience', 'specialties')
|
||||
search_fields = ('user__email', 'user__first_name', 'user__last_name', 'brokerage_name', 'license_number', 'city')
|
||||
#filter_horizontal = ('specialties', 'licensed_states')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('user', 'brokerage_name', 'license_number', 'phone_number', 'address', 'city', 'state', 'zip_code', 'bio', 'profile_picture', 'website', 'agent_type')
|
||||
}),
|
||||
('Professional Details', {
|
||||
'fields': ('specialties', 'years_experience', 'licensed_states')
|
||||
}),
|
||||
('Location', {
|
||||
'fields': ('latitude', 'longitude'),
|
||||
}),
|
||||
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
|
||||
@admin.register(UserViewModel)
|
||||
class UserViewModelAdmin(admin.ModelAdmin):
|
||||
list_display = ('pk','user','created_at')
|
||||
readonly_fields = ('created_at',)
|
||||
|
||||
@admin.register(PropertySave)
|
||||
class PropertySaveAdmin(admin.ModelAdmin):
|
||||
list_display = ('pk', 'user','property')
|
||||
readonly_fields = ('created_at',)
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.register(PropertyOwner, PropertyOwnerAdmin)
|
||||
admin.site.register(Vendor, VendorAdmin)
|
||||
admin.site.register(Property, PropertyAdmin)
|
||||
admin.site.register(VideoCategory, VideoCategoryAdmin)
|
||||
admin.site.register(Video, VideoAdmin)
|
||||
admin.site.register(UserVideoProgress, UserVideoProgressAdmin)
|
||||
admin.site.register(Conversation, ConversationAdmin)
|
||||
admin.site.register(Offer, OfferAdmin)
|
||||
admin.site.register(PropertyPictures, PropertyPicturesAdmin)
|
||||
|
||||
admin.site.register(OpenHouse, OpenHouseAdmin)
|
||||
admin.site.register(PropertySaleInfo, PropertySaleInfoAdmin)
|
||||
admin.site.register(PropertyTaxInfo, PropertyTaxInfoAdmin)
|
||||
admin.site.register(PropertyWalkScoreInfo, PropertyWalkScoreInfoAdmin)
|
||||
admin.site.register(SchoolInfo, SchoolInfoAdmin)
|
||||
|
||||
@@ -2,5 +2,5 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'core'
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "core"
|
||||
|
||||
@@ -5,92 +5,119 @@ from django.contrib.auth import get_user_model
|
||||
from rest_framework_simplejwt.tokens import AccessToken
|
||||
from .models import Conversation, Message
|
||||
from .serializers import MessageSerializer
|
||||
from .services.moderation_classifier import moderation_classifier, ModerationLabel
|
||||
from .services.llm_service import AsyncLLMService
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ChatConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
self.conversation_id = self.scope['url_route']['kwargs']['conversation_id']
|
||||
self.conversation_group_name = f'chat_{self.conversation_id}'
|
||||
|
||||
print(self.scope)
|
||||
|
||||
self.account_id = self.scope["url_route"]["kwargs"]["account_id"]
|
||||
self.account_group_name = f"chat_{self.account_id}"
|
||||
|
||||
# Authenticate user via JWT
|
||||
token = self.scope.get('query_string').decode('utf-8').split('=')[1]
|
||||
|
||||
token = self.scope.get("query_string").decode("utf-8").split("=")[1]
|
||||
|
||||
try:
|
||||
access_token = AccessToken(token)
|
||||
user_id = access_token['user_id']
|
||||
user_id = access_token["user_id"]
|
||||
self.user = await self.get_user(user_id)
|
||||
|
||||
# Check if user is part of the conversation
|
||||
conversation = await self.get_conversation(self.conversation_id)
|
||||
if not await self.is_participant(conversation, self.user):
|
||||
await self.close()
|
||||
return
|
||||
|
||||
await self.channel_layer.group_add(
|
||||
self.conversation_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
|
||||
# # Check if user is part of the conversation
|
||||
# conversation = await self.get_conversation(self.conversation_id)
|
||||
# if not await self.is_participant(conversation, self.user):
|
||||
# await self.close()
|
||||
# return
|
||||
#
|
||||
|
||||
|
||||
# await self.channel_layer.group_add(
|
||||
# self.account_group_name, self.account_id
|
||||
# )
|
||||
|
||||
await self.accept()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
await self.close()
|
||||
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.channel_layer.group_discard(
|
||||
self.conversation_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
if (self.channel_layer):
|
||||
await self.channel_layer.group_discard(
|
||||
self.account_group_name, self.account_id
|
||||
)
|
||||
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message = text_data_json['message']
|
||||
|
||||
# Save message to database
|
||||
conversation = await self.get_conversation(self.conversation_id)
|
||||
message_obj = await self.create_message(conversation, self.user, message)
|
||||
|
||||
# Serialize message
|
||||
serializer = MessageSerializer(message_obj)
|
||||
|
||||
# Send message to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.conversation_group_name,
|
||||
{
|
||||
'type': 'chat_message',
|
||||
'message': serializer.data
|
||||
}
|
||||
)
|
||||
|
||||
print(text_data_json)
|
||||
"""
|
||||
First see if it is NSFW
|
||||
|
||||
Then to the conversation
|
||||
"""
|
||||
messages = text_data_json.get('messages')
|
||||
|
||||
moderation_result = await moderation_classifier.classify_async(messages[-1])
|
||||
if moderation_result == ModerationLabel.NSFW:
|
||||
await self.send('BEGINING_OF_THE_WORLD')
|
||||
await self.send(str('Try again'))
|
||||
await self.send('END_OF_THE_WORLD')
|
||||
|
||||
|
||||
await self.send('BEGINING_OF_THE_WORLD')
|
||||
service = AsyncLLMService()
|
||||
response = ''
|
||||
# get the account to add to the prompt
|
||||
print('generating')
|
||||
async for chunk in service.generate_response(
|
||||
messages, self.user
|
||||
):
|
||||
response += chunk
|
||||
await self.send(chunk)
|
||||
|
||||
print(response)
|
||||
|
||||
await self.send('END_OF_THE_WORLD')
|
||||
|
||||
# # Save message to database
|
||||
# conversation = await self.get_conversation(self.conversation_id)
|
||||
# message_obj = await self.create_message(conversation, self.user, message)
|
||||
|
||||
# # Serialize message
|
||||
# serializer = MessageSerializer(message_obj)
|
||||
|
||||
# # Send message to room group
|
||||
# await self.channel_layer.group_send(
|
||||
# self.account_group_name,
|
||||
# {"type": "chat_message", "message": serializer.data},
|
||||
# )
|
||||
|
||||
|
||||
|
||||
async def chat_message(self, event):
|
||||
message = event['message']
|
||||
|
||||
message = event["message"]
|
||||
|
||||
# Send message to WebSocket
|
||||
await self.send(text_data=json.dumps({
|
||||
'message': message
|
||||
}))
|
||||
|
||||
await self.send(text_data=json.dumps({"message": message}))
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user(self, user_id):
|
||||
return User.objects.get(id=user_id)
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_conversation(self, conversation_id):
|
||||
return Conversation.objects.get(id=conversation_id)
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def is_participant(self, conversation, user):
|
||||
if user.user_type == 'property_owner':
|
||||
if user.user_type == "property_owner":
|
||||
return conversation.property_owner.user == user
|
||||
elif user.user_type == 'vendor':
|
||||
elif user.user_type == "vendor":
|
||||
return conversation.vendor.user == user
|
||||
return False
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def create_message(self, conversation, user, text):
|
||||
return Message.objects.create(
|
||||
conversation=conversation,
|
||||
sender=user,
|
||||
text=text
|
||||
)
|
||||
return Message.objects.create(conversation=conversation, sender=user, text=text)
|
||||
|
||||
19
dta_service/core/filters.py
Normal file
19
dta_service/core/filters.py
Normal 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']
|
||||
@@ -13,154 +13,385 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
name="User",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('first_name', models.CharField(max_length=30)),
|
||||
('last_name', models.CharField(max_length=30)),
|
||||
('user_type', models.CharField(choices=[('property_owner', 'Property Owner'), ('vendor', 'Vendor'), ('admin', 'Admin')], max_length=20)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_staff', models.BooleanField(default=False)),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254, unique=True)),
|
||||
("first_name", models.CharField(max_length=30)),
|
||||
("last_name", models.CharField(max_length=30)),
|
||||
(
|
||||
"user_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("property_owner", "Property Owner"),
|
||||
("vendor", "Vendor"),
|
||||
("admin", "Admin"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("is_staff", models.BooleanField(default=False)),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Conversation',
|
||||
name="Conversation",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Property',
|
||||
name="Property",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('address', models.CharField(max_length=200)),
|
||||
('city', models.CharField(max_length=100)),
|
||||
('state', models.CharField(max_length=2)),
|
||||
('zip_code', models.CharField(max_length=10)),
|
||||
('market_value', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('loan_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||
('loan_interest_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||
('loan_term', models.IntegerField(blank=True, null=True)),
|
||||
('loan_start_date', models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("address", models.CharField(max_length=200)),
|
||||
("city", models.CharField(max_length=100)),
|
||||
("state", models.CharField(max_length=2)),
|
||||
("zip_code", models.CharField(max_length=10)),
|
||||
("market_value", models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
(
|
||||
"loan_amount",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=12, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"loan_interest_rate",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=5, null=True
|
||||
),
|
||||
),
|
||||
("loan_term", models.IntegerField(blank=True, null=True)),
|
||||
("loan_start_date", models.DateField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VideoCategory',
|
||||
name="VideoCategory",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
("description", models.TextField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PropertyOwner',
|
||||
name="PropertyOwner",
|
||||
fields=[
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('phone_number', models.CharField(blank=True, max_length=20, null=True)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone_number",
|
||||
models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Vendor',
|
||||
name="Vendor",
|
||||
fields=[
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('business_name', models.CharField(max_length=100)),
|
||||
('business_type', models.CharField(choices=[('contractor', 'Contractor'), ('inspector', 'Inspector'), ('lender', 'Lender'), ('other', 'Other')], max_length=20)),
|
||||
('phone_number', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('address', models.CharField(max_length=200)),
|
||||
('city', models.CharField(max_length=100)),
|
||||
('state', models.CharField(max_length=2)),
|
||||
('zip_code', models.CharField(max_length=10)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
("business_name", models.CharField(max_length=100)),
|
||||
(
|
||||
"business_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("contractor", "Contractor"),
|
||||
("inspector", "Inspector"),
|
||||
("lender", "Lender"),
|
||||
("other", "Other"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone_number",
|
||||
models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
("address", models.CharField(max_length=200)),
|
||||
("city", models.CharField(max_length=100)),
|
||||
("state", models.CharField(max_length=2)),
|
||||
("zip_code", models.CharField(max_length=10)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
name="Message",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('text', models.TextField()),
|
||||
('attachment', models.FileField(blank=True, null=True, upload_to=core.models.message_file_path)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('read', models.BooleanField(default=False)),
|
||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='core.conversation')),
|
||||
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("text", models.TextField()),
|
||||
(
|
||||
"attachment",
|
||||
models.FileField(
|
||||
blank=True, null=True, upload_to=core.models.message_file_path
|
||||
),
|
||||
),
|
||||
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||
("read", models.BooleanField(default=False)),
|
||||
(
|
||||
"conversation",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="messages",
|
||||
to="core.conversation",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sender",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sent_messages",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PasswordResetToken',
|
||||
name="PasswordResetToken",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('used', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"token",
|
||||
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("expires_at", models.DateTimeField()),
|
||||
("used", models.BooleanField(default=False)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='conversation',
|
||||
name='property',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.property'),
|
||||
model_name="conversation",
|
||||
name="property",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="conversations",
|
||||
to="core.property",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Video',
|
||||
name="Video",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('link', models.URLField()),
|
||||
('duration', models.IntegerField(help_text='Duration in seconds')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='videos', to='core.videocategory')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=200)),
|
||||
("description", models.TextField(blank=True, null=True)),
|
||||
("link", models.URLField()),
|
||||
("duration", models.IntegerField(help_text="Duration in seconds")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="videos",
|
||||
to="core.videocategory",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='property',
|
||||
name='owner',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='core.propertyowner'),
|
||||
model_name="property",
|
||||
name="owner",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="properties",
|
||||
to="core.propertyowner",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='conversation',
|
||||
name='property_owner',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.propertyowner'),
|
||||
model_name="conversation",
|
||||
name="property_owner",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="conversations",
|
||||
to="core.propertyowner",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='conversation',
|
||||
name='vendor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='core.vendor'),
|
||||
model_name="conversation",
|
||||
name="vendor",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="conversations",
|
||||
to="core.vendor",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserVideoProgress',
|
||||
name="UserVideoProgress",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('progress', models.IntegerField(default=0, help_text='Progress in seconds')),
|
||||
('status', models.CharField(choices=[('not_started', 'Not Started'), ('in_progress', 'In Progress'), ('completed', 'Completed')], default='not_started', max_length=20)),
|
||||
('last_watched', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='video_progress', to=settings.AUTH_USER_MODEL)),
|
||||
('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_progress', to='core.video')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"progress",
|
||||
models.IntegerField(default=0, help_text="Progress in seconds"),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("not_started", "Not Started"),
|
||||
("in_progress", "In Progress"),
|
||||
("completed", "Completed"),
|
||||
],
|
||||
default="not_started",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("last_watched", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="video_progress",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"video",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="user_progress",
|
||||
to="core.video",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'video')},
|
||||
"unique_together": {("user", "video")},
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='conversation',
|
||||
unique_together={('property_owner', 'vendor', 'property')},
|
||||
name="conversation",
|
||||
unique_together={("property_owner", "vendor", "property")},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,12 +6,12 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
("core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='conversation',
|
||||
name="conversation",
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
18
dta_service/core/migrations/0004_user_tos_signed.py
Normal file
18
dta_service/core/migrations/0004_user_tos_signed.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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/"),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
26
dta_service/core/migrations/0011_user_tier.py
Normal file
26
dta_service/core/migrations/0011_user_tier.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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"),
|
||||
),
|
||||
]
|
||||
@@ -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")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
18
dta_service/core/migrations/0020_vendor_views.py
Normal file
18
dta_service/core/migrations/0020_vendor_views.py
Normal 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),
|
||||
),
|
||||
]
|
||||
37
dta_service/core/migrations/0021_userviewmodel.py
Normal file
37
dta_service/core/migrations/0021_userviewmodel.py
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
46
dta_service/core/migrations/0023_propertysave.py
Normal file
46
dta_service/core/migrations/0023_propertysave.py
Normal 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")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,9 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser,
|
||||
BaseUserManager,
|
||||
PermissionsMixin,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
@@ -8,33 +12,11 @@ from django.conf import settings
|
||||
import uuid
|
||||
import os
|
||||
|
||||
class TimeInfoBase(models.Model):
|
||||
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
last_modified = models.DateTimeField(default=timezone.now)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not kwargs.pop("skip_last_modified", False) and not hasattr(
|
||||
self, "skip_last_modified"
|
||||
):
|
||||
self.last_modified = timezone.now()
|
||||
if kwargs.get("update_fields") is not None:
|
||||
kwargs["update_fields"] = list(
|
||||
{*kwargs["update_fields"], "last_modified"}
|
||||
)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
if not email:
|
||||
raise ValueError('Users must have an email address')
|
||||
|
||||
raise ValueError("Users must have an email address")
|
||||
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(email=email, **extra_fields)
|
||||
user.set_password(password)
|
||||
@@ -42,17 +24,25 @@ class UserManager(BaseUserManager):
|
||||
return user
|
||||
|
||||
def create_superuser(self, email, password=None, **extra_fields):
|
||||
extra_fields.setdefault('is_staff', True)
|
||||
extra_fields.setdefault('is_superuser', True)
|
||||
extra_fields.setdefault("is_staff", True)
|
||||
extra_fields.setdefault("is_superuser", True)
|
||||
return self.create_user(email, password, **extra_fields)
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
USER_TYPE_CHOICES = (
|
||||
('property_owner', 'Property Owner'),
|
||||
('vendor', 'Vendor'),
|
||||
('admin', 'Admin'),
|
||||
("property_owner", "Property Owner"),
|
||||
("vendor", "Vendor"),
|
||||
("attorney", "Attorney"),
|
||||
("real_estate_agent", "Real Estate Agent"),
|
||||
("admin", "Admin"),
|
||||
)
|
||||
|
||||
USER_TIER_CHOICES = (
|
||||
("basic", "Basic"),
|
||||
("premium", "Premium"),
|
||||
("vendor", "Vendor"),
|
||||
)
|
||||
|
||||
email = models.EmailField(unique=True)
|
||||
first_name = models.CharField(max_length=30)
|
||||
last_name = models.CharField(max_length=30)
|
||||
@@ -60,36 +50,47 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_staff = models.BooleanField(default=False)
|
||||
date_joined = models.DateTimeField(default=timezone.now)
|
||||
|
||||
tos_signed = models.BooleanField(default=False)
|
||||
profile_created = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
tier = models.CharField(max_length=20, choices=USER_TIER_CHOICES, default="basic")
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['first_name', 'last_name', 'user_type']
|
||||
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = ["first_name", "last_name", "user_type"]
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
|
||||
def get_full_name(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
|
||||
def get_short_name(self):
|
||||
return self.first_name
|
||||
|
||||
|
||||
class PropertyOwner(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.get_full_name()
|
||||
|
||||
|
||||
class Vendor(models.Model):
|
||||
BUSINESS_TYPES = (
|
||||
('contractor', 'Contractor'),
|
||||
('inspector', 'Inspector'),
|
||||
('lender', 'Lender'),
|
||||
('other', 'Other'),
|
||||
("electrician", "Electrician"),
|
||||
("carpenter", "Carpenter"),
|
||||
("plumber", "Plumber"),
|
||||
("inspector", "Inspector"),
|
||||
("lender", "Lender"),
|
||||
("other", "Other"),
|
||||
)
|
||||
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
business_name = models.CharField(max_length=100)
|
||||
business_type = models.CharField(max_length=20, choices=BUSINESS_TYPES)
|
||||
@@ -98,121 +99,365 @@ class Vendor(models.Model):
|
||||
city = models.CharField(max_length=100)
|
||||
state = models.CharField(max_length=2)
|
||||
zip_code = models.CharField(max_length=10)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
description = models.CharField(max_length=1024, default="")
|
||||
website = models.URLField(blank=True, null=True)
|
||||
services = models.JSONField(blank=True, default=list) # Changed to JSONField
|
||||
service_areas = models.JSONField(blank=True, default=list) # Changed to JSONField
|
||||
certifications = models.JSONField(
|
||||
blank=True, null=True, default=list
|
||||
) # Changed to JSONField
|
||||
average_rating = models.DecimalField(
|
||||
max_digits=3, decimal_places=2, blank=True, null=True
|
||||
)
|
||||
num_reviews = models.IntegerField(blank=True, null=True)
|
||||
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
||||
|
||||
latitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
longitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
views = models.IntegerField(default=0)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.business_name
|
||||
|
||||
class Property(models.Model):
|
||||
owner = models.ForeignKey(PropertyOwner, on_delete=models.CASCADE, related_name='properties')
|
||||
class Attorney(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
firm_name = models.CharField(max_length=200)
|
||||
bar_number = models.CharField(max_length=50, unique=True, blank=True, null=True) # Bar numbers are typically unique
|
||||
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
||||
address = models.CharField(max_length=200)
|
||||
city = models.CharField(max_length=100)
|
||||
state = models.CharField(max_length=2)
|
||||
zip_code = models.CharField(max_length=10)
|
||||
specialties = models.JSONField(blank=True, default=list) # Store as JSON array
|
||||
years_experience = models.IntegerField(default=0)
|
||||
website = models.URLField(blank=True, null=True)
|
||||
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
||||
bio = models.TextField(blank=True, null=True) # Use TextField for longer text
|
||||
licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
latitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
longitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.get_full_name()} ({self.firm_name})"
|
||||
|
||||
class RealEstateAgent(models.Model):
|
||||
AGENT_TYPE_CHOICES = (
|
||||
("buyer_agent", "Buyer's Agent"),
|
||||
("seller_agent", "Seller's Agent"),
|
||||
("dual_agent", "Dual Agent"),
|
||||
("other", "Other"),
|
||||
)
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
brokerage_name = models.CharField(max_length=200)
|
||||
license_number = models.CharField(max_length=50, unique=True) # License numbers are typically unique
|
||||
phone_number = models.CharField(max_length=20, blank=True, null=True)
|
||||
address = models.CharField(max_length=200)
|
||||
city = models.CharField(max_length=100)
|
||||
state = models.CharField(max_length=2)
|
||||
zip_code = models.CharField(max_length=10)
|
||||
specialties = models.JSONField(blank=True, default=list) # Store as JSON array
|
||||
years_experience = models.IntegerField(default=0)
|
||||
website = models.URLField(blank=True, null=True)
|
||||
profile_picture = models.URLField(max_length=500, blank=True, null=True)
|
||||
bio = models.TextField(blank=True, null=True)
|
||||
licensed_states = models.JSONField(blank=True, default=list) # Store as JSON array
|
||||
agent_type = models.CharField(max_length=20, choices=AGENT_TYPE_CHOICES, default="other")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
latitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
longitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.get_full_name()} ({self.brokerage_name})"
|
||||
|
||||
|
||||
class UserViewModel(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
|
||||
class Property(models.Model):
|
||||
PROPERTY_STATUS_TYPES = (
|
||||
("active", "Active"),
|
||||
("pending", "Pending"),
|
||||
("contingent", "Contingent"),
|
||||
("sold", "Sold"),
|
||||
("off_market", "Off Market"),
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
PropertyOwner, on_delete=models.CASCADE, related_name="properties"
|
||||
)
|
||||
address = models.CharField(max_length=200)
|
||||
street = models.CharField(max_length=200, default="")
|
||||
city = models.CharField(max_length=100)
|
||||
state = models.CharField(max_length=2)
|
||||
zip_code = models.CharField(max_length=10)
|
||||
market_value = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
loan_amount = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True)
|
||||
loan_interest_rate = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)
|
||||
loan_amount = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, blank=True, null=True
|
||||
)
|
||||
loan_interest_rate = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, blank=True, null=True
|
||||
)
|
||||
loan_term = models.IntegerField(blank=True, null=True)
|
||||
loan_start_date = models.DateField(blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
description = models.TextField(
|
||||
blank=True, null=True
|
||||
) # Text field for longer descriptions
|
||||
sq_ft = models.IntegerField(blank=True, null=True) # Square footage
|
||||
features = models.JSONField(blank=True, default=list) # Stores a list of strings
|
||||
num_bedrooms = models.IntegerField(blank=True, null=True)
|
||||
num_bathrooms = models.IntegerField(blank=True, null=True)
|
||||
latitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
longitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
realestate_api_id = models.IntegerField()
|
||||
|
||||
property_status = models.CharField(
|
||||
max_length=15, choices=PROPERTY_STATUS_TYPES, default="off_market"
|
||||
)
|
||||
listed_price = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, blank=True, null=True
|
||||
)
|
||||
views = models.IntegerField(default=0)
|
||||
saves = models.IntegerField(default=0)
|
||||
listed_date = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.address}, {self.city}, {self.state} {self.zip_code}"
|
||||
|
||||
class SchoolInfo(models.Model):
|
||||
SCHOOL_TYPES = (
|
||||
("Public", "Public"),
|
||||
("Other", "Other"),
|
||||
)
|
||||
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name="schools", blank=True, null=True)
|
||||
city = models.CharField(max_length=100)
|
||||
state = models.CharField(max_length=2)
|
||||
zip_code = models.CharField(max_length=10)
|
||||
latitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
longitude = models.DecimalField(
|
||||
max_digits=30, decimal_places=27, blank=True, null=True
|
||||
) # For coordinates
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
enrollment = models.IntegerField()
|
||||
grades = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=256)
|
||||
parent_rating = models.IntegerField()
|
||||
rating = models.IntegerField()
|
||||
school_type = models.CharField(
|
||||
max_length=15, choices=SCHOOL_TYPES, default="public"
|
||||
)
|
||||
|
||||
class PropertyTaxInfo(models.Model):
|
||||
property = models.OneToOneField(Property, on_delete=models.CASCADE, related_name="tax_info")
|
||||
assessed_value = models.IntegerField()
|
||||
assessment_year = models.IntegerField()
|
||||
tax_amount = models.FloatField()
|
||||
year = models.IntegerField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class PropertySaleInfo(models.Model):
|
||||
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name="sale_info")
|
||||
seq_no = models.IntegerField()
|
||||
sale_date = models.DateTimeField()
|
||||
sale_amount = models.FloatField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class PropertyWalkScoreInfo(models.Model):
|
||||
property = models.OneToOneField(
|
||||
Property, on_delete=models.CASCADE, blank=True, null=True, related_name="walk_score"
|
||||
)
|
||||
walk_score = models.IntegerField()
|
||||
walk_description = models.CharField(max_length=256)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
ws_link = models.URLField()
|
||||
logo_url = models.URLField()
|
||||
transit_score = models.IntegerField()
|
||||
transit_description = models.CharField(max_length=256)
|
||||
transit_summary = models.CharField(max_length=512)
|
||||
bike_score = models.IntegerField()
|
||||
bike_description = models.CharField(max_length=256)
|
||||
|
||||
|
||||
class OpenHouse(models.Model):
|
||||
property = models.ForeignKey(Property, on_delete=models.CASCADE, blank=True, null=True, related_name="open_houses")
|
||||
listed_date = models.DateTimeField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class PropertyPictures(models.Model):
|
||||
Property = models.ForeignKey(
|
||||
Property, on_delete=models.CASCADE, related_name="pictures"
|
||||
)
|
||||
image = models.FileField(upload_to="pcitures/")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class VideoCategory(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Video(models.Model):
|
||||
category = models.ForeignKey(VideoCategory, on_delete=models.CASCADE, related_name='videos')
|
||||
category = models.ForeignKey(
|
||||
VideoCategory, on_delete=models.CASCADE, related_name="videos"
|
||||
)
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
link = models.URLField()
|
||||
link = models.FileField(upload_to="videos/")
|
||||
duration = models.IntegerField(help_text="Duration in seconds")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class UserVideoProgress(models.Model):
|
||||
STATUS_CHOICES = (
|
||||
('not_started', 'Not Started'),
|
||||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
("not_started", "Not Started"),
|
||||
("in_progress", "In Progress"),
|
||||
("completed", "Completed"),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="video_progress"
|
||||
)
|
||||
video = models.ForeignKey(
|
||||
Video, on_delete=models.CASCADE, related_name="user_progress"
|
||||
)
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='video_progress')
|
||||
video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='user_progress')
|
||||
progress = models.IntegerField(default=0, help_text="Progress in seconds")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='not_started')
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default="not_started"
|
||||
)
|
||||
last_watched = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'video')
|
||||
|
||||
unique_together = ("user", "video")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email} - {self.video.title} - {self.status}"
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Update status based on progress
|
||||
if self.progress == 0:
|
||||
self.status = 'not_started'
|
||||
self.status = "not_started"
|
||||
elif self.progress >= self.video.duration:
|
||||
self.status = 'completed'
|
||||
self.status = "completed"
|
||||
self.progress = self.video.duration
|
||||
else:
|
||||
self.status = 'in_progress'
|
||||
self.status = "in_progress"
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Conversation(models.Model):
|
||||
property_owner = models.ForeignKey(PropertyOwner, on_delete=models.CASCADE, related_name='conversations')
|
||||
vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='conversations')
|
||||
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name='conversations')
|
||||
property_owner = models.ForeignKey(
|
||||
PropertyOwner, on_delete=models.CASCADE, related_name="conversations"
|
||||
)
|
||||
vendor = models.ForeignKey(
|
||||
Vendor, on_delete=models.CASCADE, related_name="conversations"
|
||||
)
|
||||
property = models.ForeignKey(
|
||||
Property,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="conversations",
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
# class Meta:
|
||||
# unique_together = ('property_owner', 'vendor', 'property')
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Conversation between {self.property_owner} and {self.vendor} about {self.property}"
|
||||
|
||||
|
||||
def message_file_path(instance, filename):
|
||||
ext = filename.split('.')[-1]
|
||||
ext = filename.split(".")[-1]
|
||||
filename = f"{uuid.uuid4()}.{ext}"
|
||||
return os.path.join('message_attachments', filename)
|
||||
return os.path.join("message_attachments", filename)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages')
|
||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
|
||||
conversation = models.ForeignKey(
|
||||
Conversation, on_delete=models.CASCADE, related_name="messages"
|
||||
)
|
||||
sender = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="sent_messages"
|
||||
)
|
||||
text = models.TextField()
|
||||
attachment = models.FileField(upload_to=message_file_path, blank=True, null=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
read = models.BooleanField(default=False)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Message from {self.sender} in {self.conversation}"
|
||||
|
||||
|
||||
class PasswordResetToken(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
used = models.BooleanField(default=False)
|
||||
|
||||
|
||||
def is_valid(self):
|
||||
return not self.used and self.expires_at > timezone.now()
|
||||
|
||||
|
||||
def send_reset_email(self):
|
||||
subject = "Password Reset Request"
|
||||
reset_url = f"{settings.FRONTEND_URL}/reset-password/{self.token}/"
|
||||
context = {
|
||||
'user': self.user,
|
||||
'reset_url': reset_url,
|
||||
"user": self.user,
|
||||
"reset_url": reset_url,
|
||||
}
|
||||
html_message = render_to_string('password_reset_email.html', context)
|
||||
html_message = render_to_string("password_reset_email.html", context)
|
||||
plain_message = strip_tags(html_message)
|
||||
send_mail(
|
||||
subject,
|
||||
@@ -222,6 +467,90 @@ class PasswordResetToken(models.Model):
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Password reset token for {self.user.email}"
|
||||
return f"Password reset token for {self.user.email}"
|
||||
|
||||
|
||||
class Offer(models.Model):
|
||||
OFFER_STATUS_TYPES = (
|
||||
("submitted", "Submitted"),
|
||||
("draft", "Draft"),
|
||||
("accepted", "Accepted"),
|
||||
("rejected", "Rejected"),
|
||||
("counter", "Counter"),
|
||||
("withdrawn", "Withdrawn"),
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
property = models.ForeignKey(Property, on_delete=models.PROTECT)
|
||||
status = models.CharField(
|
||||
max_length=10, choices=OFFER_STATUS_TYPES, default="draft"
|
||||
)
|
||||
previous_offer = models.ForeignKey(
|
||||
"self", on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email} {self.status} {self.property.address}"
|
||||
|
||||
class Bid(models.Model):
|
||||
BID_TYPE_CHOICES = (
|
||||
("electrical", "Electrical"),
|
||||
("plumbing", "Plumbing"),
|
||||
("carpentry", "Carpentry"),
|
||||
("general_contractor", "General Contractor"),
|
||||
)
|
||||
LOCATION_CHOICES = (
|
||||
("living_room", "Living Room"),
|
||||
("basement", "Basement"),
|
||||
("kitchen", "Kitchen"),
|
||||
("bathroom", "Bathroom"),
|
||||
("bedroom", "Bedroom"),
|
||||
("outside", "Outside"),
|
||||
)
|
||||
|
||||
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name="bids")
|
||||
description = models.TextField()
|
||||
bid_type = models.CharField(max_length=50, choices=BID_TYPE_CHOICES)
|
||||
location = models.CharField(max_length=50, choices=LOCATION_CHOICES)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Bid for {self.bid_type} at {self.property.address}"
|
||||
|
||||
class BidImage(models.Model):
|
||||
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="images")
|
||||
image = models.FileField(upload_to="bid_pictures/")
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class BidResponse(models.Model):
|
||||
RESPONSE_STATUS_CHOICES = (
|
||||
("draft", "Draft"),
|
||||
("submitted", "Submitted"),
|
||||
("selected", "Selected"),
|
||||
)
|
||||
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, related_name="responses")
|
||||
vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name="bid_responses")
|
||||
description = models.TextField()
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
status = models.CharField(max_length=20, choices=RESPONSE_STATUS_CHOICES, default="draft")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('bid', 'vendor')
|
||||
|
||||
def __str__(self):
|
||||
return f"Response from {self.vendor.business_name} for Bid {self.bid.id}"
|
||||
|
||||
class PropertySave(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
property = models.ForeignKey(Property, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'property')
|
||||
|
||||
@@ -1,43 +1,63 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class IsOwnerOrReadOnly(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
if hasattr(obj, 'owner'):
|
||||
|
||||
if hasattr(obj, "owner"):
|
||||
return obj.owner.user == request.user
|
||||
elif hasattr(obj, 'user'):
|
||||
elif hasattr(obj, "user"):
|
||||
return obj.user == request.user
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class IsPropertyOwner(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return request.user.user_type == 'property_owner'
|
||||
|
||||
return request.user.user_type == "property_owner"
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if hasattr(obj, 'owner'):
|
||||
if hasattr(obj, "owner"):
|
||||
return obj.owner.user == request.user
|
||||
elif hasattr(obj, 'property_owner'):
|
||||
elif hasattr(obj, "property_owner"):
|
||||
return obj.property_owner.user == request.user
|
||||
return False
|
||||
|
||||
|
||||
class IsVendor(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return request.user.user_type == 'vendor'
|
||||
|
||||
return request.user.user_type == "vendor"
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if hasattr(obj, 'vendor'):
|
||||
if hasattr(obj, "vendor"):
|
||||
return obj.vendor.user == request.user
|
||||
return False
|
||||
|
||||
|
||||
class IsParticipant(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.user.user_type == 'property_owner':
|
||||
owner = obj.property_owner if hasattr(obj, 'property_owner') else obj.conversation.property_owner
|
||||
if request.user.user_type == "property_owner":
|
||||
owner = (
|
||||
obj.property_owner
|
||||
if hasattr(obj, "property_owner")
|
||||
else obj.conversation.property_owner
|
||||
)
|
||||
return owner.user == request.user
|
||||
elif request.user.user_type == 'vendor':
|
||||
vendor = obj.vendor if hasattr(obj, 'vendor') else obj.conversation.vendor
|
||||
elif request.user.user_type == "vendor":
|
||||
vendor = obj.vendor if hasattr(obj, "vendor") else obj.conversation.vendor
|
||||
return vendor.user == request.user
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class IsParticipantInOffer(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""
|
||||
There are two options, either you are the sender or the owner of the property
|
||||
"""
|
||||
if hasattr(obj, "user") and hasattr(obj, "property"):
|
||||
|
||||
return request.user == obj.user or request.user == obj.property.owner.user
|
||||
return False
|
||||
|
||||
@@ -2,5 +2,6 @@ from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<conversation_id>\w+)/$', consumers.ChatConsumer.as_asgi()),
|
||||
]
|
||||
re_path(r"ws/chat/(?P<account_id>\w+)/$", consumers.ChatConsumer.as_asgi()),
|
||||
#re_path(r"ws/chat/$", consumers.ChatConsumer.as_asgi()),
|
||||
]
|
||||
|
||||
@@ -3,8 +3,23 @@ from django.contrib.auth import get_user_model
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from .models import (
|
||||
PropertyOwner, Vendor, Property, VideoCategory, Video,
|
||||
UserVideoProgress, Conversation, Message, PasswordResetToken
|
||||
PropertyOwner,
|
||||
Vendor,
|
||||
Property,
|
||||
VideoCategory,
|
||||
Video,
|
||||
UserVideoProgress,
|
||||
Conversation,
|
||||
Message,
|
||||
PasswordResetToken,
|
||||
Offer,
|
||||
PropertyPictures,
|
||||
OpenHouse,
|
||||
PropertySaleInfo,
|
||||
PropertyTaxInfo,
|
||||
PropertyWalkScoreInfo,
|
||||
SchoolInfo,
|
||||
Bid, BidImage, BidResponse, RealEstateAgent, Attorney, UserViewModel, PropertySave
|
||||
)
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
@@ -15,314 +30,813 @@ from datetime import datetime, timedelta
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||
@classmethod
|
||||
def get_token(cls, user):
|
||||
token = super().get_token(user)
|
||||
|
||||
|
||||
# Add custom claims
|
||||
token['email'] = user.email
|
||||
token['first_name'] = user.first_name
|
||||
token['last_name'] = user.last_name
|
||||
token['user_type'] = user.user_type
|
||||
|
||||
token["email"] = user.email
|
||||
token["first_name"] = user.first_name
|
||||
token["last_name"] = user.last_name
|
||||
token["user_type"] = user.user_type
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def validate(self, attrs):
|
||||
data = super().validate(attrs)
|
||||
|
||||
|
||||
# Add additional responses
|
||||
refresh = self.get_token(self.user)
|
||||
data['refresh'] = str(refresh)
|
||||
data['access'] = str(refresh.access_token)
|
||||
|
||||
data["refresh"] = str(refresh)
|
||||
data["access"] = str(refresh.access_token)
|
||||
|
||||
# Add user details
|
||||
data['user'] = {
|
||||
'email': self.user.email,
|
||||
'first_name': self.user.first_name,
|
||||
'last_name': self.user.last_name,
|
||||
'user_type': self.user.user_type,
|
||||
data["user"] = {
|
||||
"email": self.user.email,
|
||||
"first_name": self.user.first_name,
|
||||
"last_name": self.user.last_name,
|
||||
"user_type": self.user.user_type,
|
||||
}
|
||||
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'email', 'first_name', 'last_name', 'user_type', 'is_active', 'date_joined']
|
||||
read_only_fields = ['id', 'is_active', 'date_joined']
|
||||
fields = [
|
||||
"id",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"user_type",
|
||||
"is_active",
|
||||
"date_joined",
|
||||
"tos_signed",
|
||||
"profile_created",
|
||||
"tier",
|
||||
]
|
||||
read_only_fields = ["id", "is_active", "date_joined"]
|
||||
|
||||
|
||||
class UserRegisterSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True, required=True)
|
||||
password2 = serializers.CharField(write_only=True, required=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['email', 'first_name', 'last_name', 'user_type', 'password', 'password2']
|
||||
fields = [
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"user_type",
|
||||
"password",
|
||||
"password2",
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': True},
|
||||
'password2': {'write_only': True},
|
||||
"password": {"write_only": True},
|
||||
"password2": {"write_only": True},
|
||||
}
|
||||
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['password'] != attrs['password2']:
|
||||
raise serializers.ValidationError({"password": "Password fields didn't match."})
|
||||
if attrs["password"] != attrs["password2"]:
|
||||
raise serializers.ValidationError(
|
||||
{"password": "Password fields didn't match."}
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data.pop('password2')
|
||||
validated_data.pop("password2")
|
||||
user = User.objects.create_user(**validated_data)
|
||||
return user
|
||||
|
||||
|
||||
class PropertyOwnerSerializer(serializers.ModelSerializer):
|
||||
user = UserSerializer()
|
||||
|
||||
|
||||
class Meta:
|
||||
model = PropertyOwner
|
||||
fields = ['user', 'phone_number']
|
||||
|
||||
fields = ["user", "phone_number"]
|
||||
read_only_fields = ["created_at", "updated_at"]
|
||||
|
||||
def create(self, validated_data):
|
||||
user_data = validated_data.pop('user')
|
||||
user_data = validated_data.pop("user")
|
||||
user = User.objects.create_user(**user_data)
|
||||
property_owner = PropertyOwner.objects.create(user=user, **validated_data)
|
||||
return property_owner
|
||||
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
user_data = validated_data.pop('user', None)
|
||||
user_data = validated_data.pop("user", None)
|
||||
if user_data:
|
||||
user = instance.user
|
||||
for attr, value in user_data.items():
|
||||
setattr(user, attr, value)
|
||||
user.save()
|
||||
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class VendorSerializer(serializers.ModelSerializer):
|
||||
user = UserSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Vendor
|
||||
fields = ['user', 'business_name', 'business_type', 'phone_number',
|
||||
'address', 'city', 'state', 'zip_code']
|
||||
|
||||
|
||||
fields = [
|
||||
# List all Vendor fields you want to expose/update, but not the user field
|
||||
"business_name",
|
||||
"business_type",
|
||||
"phone_number",
|
||||
"address",
|
||||
"city",
|
||||
"state",
|
||||
"zip_code",
|
||||
"description",
|
||||
"website",
|
||||
"services",
|
||||
"service_areas",
|
||||
"certifications",
|
||||
"longitude",
|
||||
"latitude",
|
||||
"profile_picture",
|
||||
"user",
|
||||
"views"
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"average_rating",
|
||||
"num_reviews",
|
||||
]
|
||||
# This create method is fine for creating a new vendor and user
|
||||
def create(self, validated_data):
|
||||
# Extract user data
|
||||
user_data = validated_data.pop('user')
|
||||
|
||||
# Get or create category
|
||||
user, _ = User.objects.get_or_create(**user_data)
|
||||
|
||||
# Create video with the category
|
||||
user_data = validated_data.pop("user")
|
||||
user = User.objects.create_user(**user_data) # Use create_user to hash the password if present
|
||||
vendor = Vendor.objects.create(user=user, **validated_data)
|
||||
return vendor
|
||||
|
||||
# Override the update method to handle the nested user data
|
||||
def update(self, instance, validated_data):
|
||||
user_data = validated_data.pop('user', None)
|
||||
|
||||
# Update Vendor fields
|
||||
# Pop the user data to handle it separately
|
||||
user_data = validated_data.pop("user", {})
|
||||
user_instance = instance.user
|
||||
|
||||
# Update the Vendor instance fields
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
# Update nested User fields if provided
|
||||
if user_data:
|
||||
user = instance.user
|
||||
email = user_data.get('email', None)
|
||||
|
||||
# Only validate email uniqueness if it's being changed
|
||||
if email and email != user.email:
|
||||
if User.objects.filter(email=email).exists():
|
||||
raise serializers.ValidationError({
|
||||
'email': 'A user with this email already exists.'
|
||||
})
|
||||
|
||||
for attr, value in user_data.items():
|
||||
setattr(user, attr, value)
|
||||
user.save()
|
||||
|
||||
|
||||
# Update the nested User instance fields
|
||||
for attr, value in user_data.items():
|
||||
setattr(user_instance, attr, value)
|
||||
user_instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
class PropertySerializer(serializers.ModelSerializer):
|
||||
|
||||
class PropertyPictureSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PropertyPictures
|
||||
fields = ["id", "created_at", "updated_at", "image", "Property"]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def create(self, validated_data):
|
||||
property_id = self.context["request"].data.get("Property")
|
||||
try:
|
||||
property_instance = Property.objects.get(id=property_id)
|
||||
|
||||
except Property.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid property ID.")
|
||||
|
||||
validated_data["Property"] = property_instance
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class OpenHouseSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OpenHouse
|
||||
fields = ["id", "created_at", "updated_at", "listed_date", "property"]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def create(self, validated_data):
|
||||
property_id = self.context["request"].data.get("property")
|
||||
try:
|
||||
property_instance = Property.objects.get(id=property_id)
|
||||
|
||||
except Property.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid property ID.")
|
||||
|
||||
validated_data["property"] = property_instance
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class SchoolInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SchoolInfo
|
||||
fields = [
|
||||
"city",
|
||||
"state",
|
||||
"zip_code",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"enrollment",
|
||||
"grades",
|
||||
"name",
|
||||
"parent_rating",
|
||||
"rating",
|
||||
"school_type",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class PropertyWalkScoreInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PropertyWalkScoreInfo
|
||||
fields = [
|
||||
"walk_score",
|
||||
"walk_description",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"ws_link",
|
||||
"logo_url",
|
||||
"transit_score",
|
||||
"transit_description",
|
||||
"transit_summary",
|
||||
"bike_score",
|
||||
"bike_description",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def create(self, validated_data):
|
||||
property_id = self.context["request"].data.get("property")
|
||||
try:
|
||||
property_instance = Property.objects.get(id=property_id)
|
||||
|
||||
except Property.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid property ID.")
|
||||
|
||||
validated_data["property"] = property_instance
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class PropertyTaxInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PropertyTaxInfo
|
||||
fields = [
|
||||
"assessed_value",
|
||||
"assessment_year",
|
||||
"tax_amount",
|
||||
"year",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class PropertySaleInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PropertySaleInfo
|
||||
fields = [
|
||||
"seq_no",
|
||||
"sale_date",
|
||||
"sale_amount",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class PropertyResponseSerializer(serializers.ModelSerializer):
|
||||
owner = PropertyOwnerSerializer()
|
||||
pictures = PropertyPictureSerializer(many=True)
|
||||
open_houses = OpenHouseSerializer(many=True)
|
||||
schools = SchoolInfoSerializer(many=True)
|
||||
walk_score = PropertyWalkScoreInfoSerializer(many=False)
|
||||
tax_info = PropertyTaxInfoSerializer(many=False)
|
||||
sale_info = PropertySaleInfoSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Property
|
||||
fields = ['id', 'owner', 'address', 'city', 'state', 'zip_code',
|
||||
'market_value', 'loan_amount', 'loan_interest_rate',
|
||||
'loan_term', 'loan_start_date']
|
||||
read_only_fields = ['id']
|
||||
fields = [
|
||||
"id",
|
||||
"owner",
|
||||
"address",
|
||||
"street",
|
||||
"city",
|
||||
"state",
|
||||
"zip_code",
|
||||
"market_value",
|
||||
"loan_amount",
|
||||
"loan_interest_rate",
|
||||
"loan_term",
|
||||
"loan_start_date",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"description",
|
||||
"sq_ft",
|
||||
"features",
|
||||
"num_bedrooms",
|
||||
"num_bathrooms",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"realestate_api_id",
|
||||
"property_status",
|
||||
"views",
|
||||
"saves",
|
||||
"listed_date",
|
||||
"pictures",
|
||||
'open_houses',
|
||||
'schools',
|
||||
'walk_score',
|
||||
'tax_info',
|
||||
'sale_info',
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class PropertyRequestSerializer(serializers.ModelSerializer):
|
||||
schools = SchoolInfoSerializer(many=True)
|
||||
tax_info = PropertyTaxInfoSerializer()
|
||||
sale_info = PropertySaleInfoSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Property
|
||||
fields = [
|
||||
"id",
|
||||
"owner",
|
||||
"address",
|
||||
"street",
|
||||
"city",
|
||||
"state",
|
||||
"zip_code",
|
||||
"market_value",
|
||||
"loan_amount",
|
||||
"loan_interest_rate",
|
||||
"loan_term",
|
||||
"loan_start_date",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"description",
|
||||
"sq_ft",
|
||||
"features",
|
||||
"num_bedrooms",
|
||||
"num_bathrooms",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"realestate_api_id",
|
||||
"property_status",
|
||||
"views",
|
||||
"saves",
|
||||
"listed_date",
|
||||
"tax_info",
|
||||
"sale_info",
|
||||
"schools"
|
||||
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at", "views", "saves"]
|
||||
def create(self, validated_data):
|
||||
# tax_info_data = validated_data.pop("tax_info")
|
||||
# tax_info_data = validated_data.pop("tax_info")
|
||||
walk_score = validated_data.pop("walk_score")
|
||||
schools_data = validated_data.pop("schools")
|
||||
tax_info = validated_data.pop("tax_info")
|
||||
sale_info = validated_data.pop("sale_info")
|
||||
schools = []
|
||||
|
||||
|
||||
|
||||
property_instance = Property.objects.create(**validated_data)
|
||||
|
||||
sale_infos = []
|
||||
for sale_in in sale_info:
|
||||
sale_infos.append(PropertySaleInfo.objects.create(**sale_in, property=property_instance))
|
||||
|
||||
|
||||
for school_data in schools_data:
|
||||
schools.append(SchoolInfo.objects.create(**school_data, property=property_instance))
|
||||
|
||||
PropertyTaxInfo.objects.create(**tax_info, property=property_instance)
|
||||
walk_score.property = property_instance
|
||||
walk_score.save()
|
||||
|
||||
return property_instance
|
||||
|
||||
|
||||
class VideoCategorySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = VideoCategory
|
||||
fields = ['id', 'name', 'description']
|
||||
read_only_fields = ['id']
|
||||
fields = ["id", "name", "description"]
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class VideoSerializer(serializers.ModelSerializer):
|
||||
category = VideoCategorySerializer()
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Video
|
||||
fields = ['id', 'category', 'title', 'description', 'link', 'duration', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
fields = [
|
||||
"id",
|
||||
"category",
|
||||
"title",
|
||||
"description",
|
||||
"link",
|
||||
"duration",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def create(self, validated_data):
|
||||
# Extract category data
|
||||
category_data = validated_data.pop('category')
|
||||
|
||||
category_data = validated_data.pop("category")
|
||||
|
||||
# Get or create category
|
||||
category, _ = VideoCategory.objects.get_or_create(**category_data)
|
||||
|
||||
|
||||
# Create video with the category
|
||||
video = Video.objects.create(category=category, **validated_data)
|
||||
return video
|
||||
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Handle category update if provided
|
||||
category_data = validated_data.pop('category', None)
|
||||
category_data = validated_data.pop("category", None)
|
||||
if category_data:
|
||||
category, _ = VideoCategory.objects.get_or_create(**category_data)
|
||||
instance.category = category
|
||||
|
||||
|
||||
# Update other fields
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class UserVideoProgressSerializer(serializers.ModelSerializer):
|
||||
video = VideoSerializer()
|
||||
|
||||
|
||||
class Meta:
|
||||
model = UserVideoProgress
|
||||
fields = ['video', 'progress', 'status', 'last_watched', 'user']
|
||||
read_only_fields = ['status', 'last_watched']
|
||||
|
||||
fields = ["id", "video", "progress", "status", "last_watched", "user"]
|
||||
read_only_fields = ["id", "status", "last_watched"]
|
||||
|
||||
def create(self, validated_data):
|
||||
# Extract video data
|
||||
video_data = validated_data.pop('video')
|
||||
|
||||
video_data = validated_data.pop("video")
|
||||
|
||||
# Get or create video
|
||||
video_serializer = VideoSerializer(data=video_data)
|
||||
video_serializer.is_valid(raise_exception=True)
|
||||
video = video_serializer.save()
|
||||
user = validated_data.pop('user')
|
||||
user = validated_data.pop("user")
|
||||
# Create progress record
|
||||
progress = UserVideoProgress.objects.create(
|
||||
user=user,
|
||||
video=video,
|
||||
**validated_data
|
||||
user=user, video=video, **validated_data
|
||||
)
|
||||
return progress
|
||||
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Handle video update if provided
|
||||
video_data = validated_data.pop('video', None)
|
||||
video_data = validated_data.pop("video", None)
|
||||
if video_data:
|
||||
video_serializer = VideoSerializer(instance.video, data=video_data, partial=True)
|
||||
video_serializer = VideoSerializer(
|
||||
instance.video, data=video_data, partial=True
|
||||
)
|
||||
video_serializer.is_valid(raise_exception=True)
|
||||
video_serializer.save()
|
||||
|
||||
|
||||
# Update progress
|
||||
instance.progress = validated_data.get('progress', instance.progress)
|
||||
instance.progress = validated_data.get("progress", instance.progress)
|
||||
instance.save()
|
||||
|
||||
|
||||
return instance
|
||||
|
||||
class ConversationSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = ['id', 'property_owner', 'vendor', 'property', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ['id', 'conversation', 'sender', 'text', 'attachment', 'timestamp', 'read']
|
||||
read_only_fields = ['id', 'timestamp']
|
||||
|
||||
fields = [
|
||||
"id",
|
||||
"conversation",
|
||||
"sender",
|
||||
"text",
|
||||
"attachment",
|
||||
"timestamp",
|
||||
"read",
|
||||
]
|
||||
read_only_fields = ["id", "timestamp"]
|
||||
|
||||
def create(self, validated_data):
|
||||
# Extract user data
|
||||
sender = validated_data.pop('sender')
|
||||
sender = validated_data.pop("sender")
|
||||
|
||||
message = Message.objects.create(sender=sender, **validated_data)
|
||||
return message
|
||||
|
||||
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
Handle updates to message fields.
|
||||
Note: sender and conversation are typically read-only in updates
|
||||
"""
|
||||
# Update text if provided
|
||||
instance.text = validated_data.get('text', instance.text)
|
||||
|
||||
instance.text = validated_data.get("text", instance.text)
|
||||
|
||||
# Update read status if provided
|
||||
if 'read' in validated_data:
|
||||
instance.read = validated_data['read']
|
||||
|
||||
if "read" in validated_data:
|
||||
instance.read = validated_data["read"]
|
||||
|
||||
# Handle attachment updates (if needed)
|
||||
if 'attachment' in validated_data:
|
||||
if "attachment" in validated_data:
|
||||
# Delete old attachment if exists
|
||||
if instance.attachment:
|
||||
instance.attachment.delete()
|
||||
instance.attachment = validated_data['attachment']
|
||||
|
||||
instance.attachment = validated_data["attachment"]
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class ConversationResponseSerializer(serializers.ModelSerializer):
|
||||
vendor = VendorSerializer()
|
||||
property_owner = PropertyOwnerSerializer()
|
||||
messages = MessageSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = [
|
||||
"id",
|
||||
"property_owner",
|
||||
"vendor",
|
||||
"property",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"messages",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class ConversationRequestSerializer(serializers.ModelSerializer):
|
||||
messages = MessageSerializer(many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = [
|
||||
"id",
|
||||
"property_owner",
|
||||
"vendor",
|
||||
"property",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"messages",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class PasswordResetRequestSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
|
||||
|
||||
def validate_email(self, value):
|
||||
try:
|
||||
User.objects.get(email=value)
|
||||
except User.DoesNotExist:
|
||||
raise serializers.ValidationError("No user with this email address exists.")
|
||||
return value
|
||||
|
||||
|
||||
def save(self):
|
||||
user = User.objects.get(email=self.validated_data['email'])
|
||||
user = User.objects.get(email=self.validated_data["email"])
|
||||
expires_at = datetime.now() + timedelta(hours=24)
|
||||
token = PasswordResetToken.objects.create(
|
||||
user=user,
|
||||
expires_at=expires_at
|
||||
)
|
||||
token = PasswordResetToken.objects.create(user=user, expires_at=expires_at)
|
||||
token.send_reset_email()
|
||||
return token
|
||||
|
||||
|
||||
class PasswordResetConfirmSerializer(serializers.Serializer):
|
||||
token = serializers.UUIDField()
|
||||
new_password = serializers.CharField(write_only=True)
|
||||
new_password2 = serializers.CharField(write_only=True)
|
||||
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
try:
|
||||
token = PasswordResetToken.objects.get(token=attrs['token'])
|
||||
token = PasswordResetToken.objects.get(token=attrs["token"])
|
||||
except PasswordResetToken.DoesNotExist:
|
||||
raise serializers.ValidationError({"token": "Invalid token."})
|
||||
|
||||
|
||||
if not token.is_valid():
|
||||
raise serializers.ValidationError({"token": "Token is invalid or has expired."})
|
||||
|
||||
if attrs['new_password'] != attrs['new_password2']:
|
||||
raise serializers.ValidationError({"new_password": "Password fields didn't match."})
|
||||
|
||||
raise serializers.ValidationError(
|
||||
{"token": "Token is invalid or has expired."}
|
||||
)
|
||||
|
||||
if attrs["new_password"] != attrs["new_password2"]:
|
||||
raise serializers.ValidationError(
|
||||
{"new_password": "Password fields didn't match."}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
def save(self):
|
||||
token = PasswordResetToken.objects.get(token=self.validated_data['token'])
|
||||
token = PasswordResetToken.objects.get(token=self.validated_data["token"])
|
||||
user = token.user
|
||||
user.set_password(self.validated_data['new_password'])
|
||||
user.set_password(self.validated_data["new_password"])
|
||||
user.save()
|
||||
token.used = True
|
||||
token.save()
|
||||
return user
|
||||
return user
|
||||
|
||||
|
||||
class OfferRequestSerializer(serializers.ModelSerializer):
|
||||
previous_offer = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Offer
|
||||
fields = ["id", "user", "property", "status", "previous_offer", "is_active"]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_previous_offer(self, model_field):
|
||||
return OfferRequestSerializer()
|
||||
|
||||
|
||||
class OfferResponseSerializer(serializers.ModelSerializer):
|
||||
previous_offer = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
user = UserSerializer()
|
||||
property = PropertyResponseSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Offer
|
||||
fields = [
|
||||
"id",
|
||||
"user",
|
||||
"property",
|
||||
"status",
|
||||
"previous_offer",
|
||||
"is_active",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_previous_offer(self, model_field):
|
||||
return OfferResponseSerializer()
|
||||
|
||||
def validate_status(self, value):
|
||||
return value
|
||||
|
||||
class BidImageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BidImage
|
||||
fields = ["id", "image"]
|
||||
|
||||
class BidResponseSerializer(serializers.ModelSerializer):
|
||||
vendor = VendorSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = BidResponse
|
||||
fields = ["id", "bid", "vendor", "description", "price", "status", "created_at", "updated_at"]
|
||||
read_only_fields = ["id", "created_at", "updated_at", "vendor"]
|
||||
|
||||
class BidSerializer(serializers.ModelSerializer):
|
||||
images = BidImageSerializer(many=True, read_only=True)
|
||||
responses = BidResponseSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Bid
|
||||
fields = ["id", "property", "description", "bid_type", "location", "created_at", "updated_at", "images", "responses"]
|
||||
read_only_fields = ["id", "created_at", "updated_at", "responses"]
|
||||
|
||||
def create(self, validated_data):
|
||||
images_data = self.context.get('request').FILES.getlist('images')
|
||||
bid = Bid.objects.create(**validated_data)
|
||||
for image_data in images_data:
|
||||
# Assuming you have an image upload logic, like storing to S3 and getting a URL
|
||||
BidImage.objects.create(bid=bid, image=image_data)
|
||||
return bid
|
||||
|
||||
class AttorneySerializer(serializers.ModelSerializer):
|
||||
user = UserSerializer(read_only=True) # Nested serializer for the related User object
|
||||
|
||||
class Meta:
|
||||
model = Attorney
|
||||
fields = [
|
||||
'user', 'firm_name', 'phone_number', 'address', 'city',
|
||||
'state', 'zip_code', 'specialties', 'years_experience', 'website',
|
||||
'profile_picture', 'bio', 'licensed_states', 'created_at', 'updated_at',
|
||||
"longitude",
|
||||
"latitude",
|
||||
]
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
def create(self, validated_data):
|
||||
# When creating an Attorney, the User object should already exist or be created separately.
|
||||
# This serializer assumes the user is already linked or passed in the context.
|
||||
# For simplicity, we'll assume the user is passed directly to the view.
|
||||
# In a real scenario, you'd handle user creation/association in the view or a custom manager.
|
||||
user_instance = self.context.get('user')
|
||||
if not user_instance:
|
||||
raise serializers.ValidationError("User instance must be provided to create an Attorney.")
|
||||
|
||||
# Ensure the user_type is correctly set for the new user
|
||||
if user_instance.user_type != 'attorney':
|
||||
user_instance.user_type = 'attorney'
|
||||
user_instance.save()
|
||||
|
||||
attorney = Attorney.objects.create(user=user_instance, **validated_data)
|
||||
return attorney
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Handle updates for Attorney fields
|
||||
instance.firm_name = validated_data.get('firm_name', instance.firm_name)
|
||||
instance.bar_number = validated_data.get('bar_number', instance.bar_number)
|
||||
instance.phone_number = validated_data.get('phone_number', instance.phone_number)
|
||||
instance.address = validated_data.get('address', instance.address)
|
||||
instance.city = validated_data.get('city', instance.city)
|
||||
instance.state = validated_data.get('state', instance.state)
|
||||
instance.zip_code = validated_data.get('zip_code', instance.zip_code)
|
||||
instance.specialties = validated_data.get('specialties', instance.specialties)
|
||||
instance.years_experience = validated_data.get('years_experience', instance.years_experience)
|
||||
instance.website = validated_data.get('website', instance.website)
|
||||
instance.profile_picture = validated_data.get('profile_picture', instance.profile_picture)
|
||||
instance.bio = validated_data.get('bio', instance.bio)
|
||||
instance.licensed_states = validated_data.get('licensed_states', instance.licensed_states)
|
||||
instance.longitude = validated_data.get('longitude', instance.longitude)
|
||||
instance.latitude = validated_data.get('latitude', instance.latitude)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class RealEstateAgentSerializer(serializers.ModelSerializer):
|
||||
user = UserSerializer(read_only=True) # Nested serializer for the related User object
|
||||
|
||||
class Meta:
|
||||
model = RealEstateAgent
|
||||
fields = [
|
||||
'user', 'brokerage_name', 'license_number', 'phone_number', 'address', 'city',
|
||||
'state', 'zip_code', 'specialties', 'years_experience', 'website',
|
||||
'profile_picture', 'bio', 'licensed_states', 'agent_type', 'created_at', 'updated_at',
|
||||
"longitude",
|
||||
"latitude",
|
||||
]
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
def create(self, validated_data):
|
||||
user_instance = self.context.get('user')
|
||||
if not user_instance:
|
||||
raise serializers.ValidationError("User instance must be provided to create a RealEstateAgent.")
|
||||
|
||||
# Ensure the user_type is correctly set for the new user
|
||||
if user_instance.user_type != 'real_estate_agent':
|
||||
user_instance.user_type = 'real_estate_agent'
|
||||
user_instance.save()
|
||||
|
||||
agent = RealEstateAgent.objects.create(user=user_instance, **validated_data)
|
||||
return agent
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Handle updates for RealEstateAgent fields
|
||||
instance.brokerage_name = validated_data.get('brokerage_name', instance.brokerage_name)
|
||||
instance.license_number = validated_data.get('license_number', instance.license_number)
|
||||
instance.phone_number = validated_data.get('phone_number', instance.phone_number)
|
||||
instance.address = validated_data.get('address', instance.address)
|
||||
instance.city = validated_data.get('city', instance.city)
|
||||
instance.state = validated_data.get('state', instance.state)
|
||||
instance.zip_code = validated_data.get('zip_code', instance.zip_code)
|
||||
instance.specialties = validated_data.get('specialties', instance.specialties)
|
||||
instance.years_experience = validated_data.get('years_experience', instance.years_experience)
|
||||
instance.website = validated_data.get('website', instance.website)
|
||||
instance.profile_picture = validated_data.get('profile_picture', instance.profile_picture)
|
||||
instance.bio = validated_data.get('bio', instance.bio)
|
||||
instance.licensed_states = validated_data.get('licensed_states', instance.licensed_states)
|
||||
instance.agent_type = validated_data.get('agent_type', instance.agent_type)
|
||||
instance.longitude = validated_data.get('longitude', instance.longitude)
|
||||
instance.latitude = validated_data.get('latitude', instance.latitude)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
class PropertySaveSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for the PropertySave model.
|
||||
"""
|
||||
property = serializers.PrimaryKeyRelatedField(queryset=Property.objects.all())
|
||||
user = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PropertySave
|
||||
fields = ['id', 'user', 'property', 'created_at']
|
||||
read_only_fields = ['created_at']
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Check for a unique user-property combination before creation.
|
||||
"""
|
||||
user = self.context['request'].user
|
||||
property_id = data.get('property').id
|
||||
if PropertySave.objects.filter(user=user, property=property_id).exists():
|
||||
raise serializers.ValidationError(
|
||||
"This property is already saved by the user."
|
||||
)
|
||||
return data
|
||||
|
||||
0
dta_service/core/services/__init__.py
Normal file
0
dta_service/core/services/__init__.py
Normal file
18
dta_service/core/services/base_llm_service.py
Normal file
18
dta_service/core/services/base_llm_service.py
Normal 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()
|
||||
136
dta_service/core/services/llm_service.py
Normal file
136
dta_service/core/services/llm_service.py
Normal 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., "Here’s 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 can’t calculate your home’s exact value, but here’s 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
|
||||
88
dta_service/core/services/moderation_classifier.py
Normal file
88
dta_service/core/services/moderation_classifier.py
Normal 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()
|
||||
102
dta_service/core/services/property_description_generator.py
Normal file
102
dta_service/core/services/property_description_generator.py
Normal 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)
|
||||
8
dta_service/core/urls/attorney.py
Normal file
8
dta_service/core/urls/attorney.py
Normal 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
|
||||
8
dta_service/core/urls/bid.py
Normal file
8
dta_service/core/urls/bid.py
Normal 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
|
||||
@@ -3,19 +3,26 @@ from rest_framework.routers import DefaultRouter
|
||||
from core.views import ConversationViewSet, MessageViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', ConversationViewSet, basename='conversation')
|
||||
router.register(r"", ConversationViewSet, basename="conversation")
|
||||
|
||||
urlpatterns = [
|
||||
path('<int:conversation_id>/messages/', MessageViewSet.as_view({
|
||||
'get': 'list',
|
||||
'post': 'create'
|
||||
}), name='conversation-messages'),
|
||||
path('<int:conversation_id>/messages/<int:pk>/', MessageViewSet.as_view({
|
||||
'get': 'retrieve',
|
||||
'put': 'update',
|
||||
'patch': 'partial_update',
|
||||
'delete': 'destroy'
|
||||
}), name='message-detail'),
|
||||
path(
|
||||
"<int:conversation_id>/messages/",
|
||||
MessageViewSet.as_view({"get": "list", "post": "create"}),
|
||||
name="conversation-messages",
|
||||
),
|
||||
path(
|
||||
"<int:conversation_id>/messages/<int:pk>/",
|
||||
MessageViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="message-detail",
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
urlpatterns += router.urls
|
||||
|
||||
8
dta_service/core/urls/offer.py
Normal file
8
dta_service/core/urls/offer.py
Normal 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
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from core.views import PropertyViewSet
|
||||
from core.views import PropertyViewSet, PropertyPictureViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', PropertyViewSet, basename='property')
|
||||
router.register(r"pictures", PropertyPictureViewSet, basename="property-pictures")
|
||||
router.register(r"", PropertyViewSet, basename="property")
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -3,6 +3,6 @@ from rest_framework.routers import DefaultRouter
|
||||
from core.views import PropertyOwnerViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', PropertyOwnerViewSet, basename='property-owner')
|
||||
router.register(r"", PropertyOwnerViewSet, basename="property-owner")
|
||||
|
||||
urlpatterns = router.urls
|
||||
urlpatterns = router.urls
|
||||
|
||||
11
dta_service/core/urls/property_save.py
Normal file
11
dta_service/core/urls/property_save.py
Normal 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)),
|
||||
]
|
||||
8
dta_service/core/urls/real_estate_agent.py
Normal file
8
dta_service/core/urls/real_estate_agent.py
Normal 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
|
||||
@@ -3,6 +3,6 @@ from rest_framework.routers import DefaultRouter
|
||||
from core.views import VendorViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', VendorViewSet, basename='vendor')
|
||||
router.register(r"", VendorViewSet, basename="vendor")
|
||||
|
||||
urlpatterns = router.urls
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -3,8 +3,9 @@ from rest_framework.routers import DefaultRouter
|
||||
from core.views import VideoCategoryViewSet, VideoViewSet, UserVideoProgressViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'categories', VideoCategoryViewSet, basename='video-category')
|
||||
router.register(r'', VideoViewSet, basename='video')
|
||||
router.register(r'progress', UserVideoProgressViewSet, basename='video-progress')
|
||||
router.register(r"categories", VideoCategoryViewSet, basename="video-category")
|
||||
router.register(r"progress", UserVideoProgressViewSet, basename="video-progress")
|
||||
router.register(r"", VideoViewSet, basename="video")
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -3,38 +3,107 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
import requests
|
||||
from rest_framework.decorators import action
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import get_object_or_404
|
||||
from .models import (
|
||||
PropertyOwner, Vendor, Property, VideoCategory, Video,
|
||||
UserVideoProgress, Conversation, Message
|
||||
PropertyOwner,
|
||||
Vendor,
|
||||
Property,
|
||||
VideoCategory,
|
||||
Video,
|
||||
UserVideoProgress,
|
||||
Conversation,
|
||||
Message,
|
||||
Offer,
|
||||
PropertyWalkScoreInfo,
|
||||
PropertyTaxInfo,
|
||||
SchoolInfo,Bid, BidResponse, Attorney, RealEstateAgent, UserViewModel, PropertySave
|
||||
)
|
||||
from .serializers import (
|
||||
CustomTokenObtainPairSerializer, UserSerializer, UserRegisterSerializer,
|
||||
PropertyOwnerSerializer, VendorSerializer, PropertySerializer,
|
||||
VideoCategorySerializer, VideoSerializer, UserVideoProgressSerializer,
|
||||
ConversationSerializer, MessageSerializer, PasswordResetRequestSerializer,
|
||||
PasswordResetConfirmSerializer
|
||||
CustomTokenObtainPairSerializer,
|
||||
UserSerializer,
|
||||
UserRegisterSerializer,
|
||||
PropertyOwnerSerializer,
|
||||
VendorSerializer,
|
||||
PropertyResponseSerializer,
|
||||
PropertyRequestSerializer,
|
||||
VideoCategorySerializer,
|
||||
VideoSerializer,
|
||||
UserVideoProgressSerializer,
|
||||
ConversationRequestSerializer,
|
||||
ConversationResponseSerializer,
|
||||
MessageSerializer,
|
||||
PasswordResetRequestSerializer,
|
||||
PasswordResetConfirmSerializer,
|
||||
OfferRequestSerializer,
|
||||
OfferResponseSerializer,
|
||||
PropertyPictureSerializer, BidSerializer, BidResponseSerializer, AttorneySerializer, RealEstateAgentSerializer, PropertySaveSerializer
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from .permissions import IsOwnerOrReadOnly, IsPropertyOwner, IsVendor, IsParticipant
|
||||
from .permissions import (
|
||||
IsOwnerOrReadOnly,
|
||||
IsPropertyOwner,
|
||||
IsVendor,
|
||||
IsParticipant,
|
||||
IsParticipantInOffer,
|
||||
)
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import filters
|
||||
from django.db.models import Q
|
||||
from .services.property_description_generator import PropertyDescriptionGenerator
|
||||
from .filters import PropertyFilterSet
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||
serializer_class = CustomTokenObtainPairSerializer
|
||||
|
||||
|
||||
class UserRegisterView(generics.CreateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserRegisterSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
|
||||
class UserRetrieveView(generics.RetrieveAPIView, generics.UpdateAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
serializer = UserSerializer(request.user)
|
||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = UserSerializer(request.user, data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||
else:
|
||||
print(serializer.errors)
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data=UserSerializer(request.user).data,
|
||||
)
|
||||
|
||||
|
||||
class UserSignTosView(generics.UpdateAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def put(self, request):
|
||||
user = User.objects.get(email=request.user.email)
|
||||
user.tos_signed = True
|
||||
user.save()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||
|
||||
|
||||
class LogoutView(APIView):
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
authentication_classes = ()
|
||||
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
refresh_token = request.data["refresh_token"]
|
||||
@@ -44,130 +113,489 @@ class LogoutView(APIView):
|
||||
except Exception as e:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class PasswordResetRequestView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
|
||||
def post(self, request):
|
||||
serializer = PasswordResetRequestSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(
|
||||
{"detail": "Password reset email has been sent."},
|
||||
status=status.HTTP_200_OK
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class PasswordResetConfirmView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
|
||||
def post(self, request):
|
||||
serializer = PasswordResetConfirmSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(
|
||||
{"detail": "Password has been reset successfully."},
|
||||
status=status.HTTP_200_OK
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class PropertyOwnerViewSet(viewsets.ModelViewSet):
|
||||
queryset = PropertyOwner.objects.all()
|
||||
serializer_class = PropertyOwnerSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
search_fields = ['user__first_name', 'user__last_name', 'user__email']
|
||||
search_fields = ["user__first_name", "user__last_name", "user__email"]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.user_type == "property_owner":
|
||||
if(PropertyOwner.objects.filter(user=user).count() == 0):
|
||||
return PropertyOwner.objects.create(
|
||||
user=user,
|
||||
)
|
||||
return PropertyOwner.objects.filter(user=user)
|
||||
else:
|
||||
return PropertyOwner.objects.all()
|
||||
|
||||
|
||||
class VendorViewSet(viewsets.ModelViewSet):
|
||||
queryset = Vendor.objects.all()
|
||||
serializer_class = VendorSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
search_fields = ['business_name', 'user__first_name', 'user__last_name', 'user__email']
|
||||
filterset_fields = ['business_type']
|
||||
search_fields = [
|
||||
"business_name",
|
||||
"user__first_name",
|
||||
"user__last_name",
|
||||
"user__email",
|
||||
]
|
||||
filterset_fields = ["business_type"]
|
||||
lookup_field = "user__id" # or 'user__id' if you want to be explicit
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['increment_view_count', 'increment_save_count']:
|
||||
permission_classes = [IsAuthenticated]
|
||||
else:
|
||||
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def get_queryset(self):
|
||||
# Your existing logic is fine here
|
||||
user = self.request.user
|
||||
if user.user_type == "vendor":
|
||||
# If the Vendor profile doesn't exist, create it
|
||||
if not Vendor.objects.filter(user=user).exists():
|
||||
return Vendor.objects.create(user=user)
|
||||
return Vendor.objects.filter(user=user)
|
||||
return Vendor.objects.all()
|
||||
|
||||
def get_object(self):
|
||||
# Override get_object to ensure the user can only access their own Vendor profile
|
||||
# when a specific ID is provided in the URL.
|
||||
queryset = self.get_queryset()
|
||||
obj = generics.get_object_or_404(queryset, user=self.request.user)
|
||||
self.check_object_permissions(self.request, obj)
|
||||
return obj
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
# The update method will now handle the vendor profile correctly
|
||||
# and ignore any user data in the payload.
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def increment_view_count(self, request, user__id=None):
|
||||
vendor_obj = Vendor.objects.get(user__id=user__id)
|
||||
vendor_obj.views += 1
|
||||
vendor_obj.save()
|
||||
UserViewModel.objects.create(
|
||||
user_id=user__id
|
||||
)
|
||||
|
||||
return Response({'views': vendor_obj.views}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
# Attorney ViewSet
|
||||
class AttorneyViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = AttorneySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# When creating an Attorney, link it to the currently authenticated user
|
||||
# or handle user creation/association logic here.
|
||||
# For demonstration, we'll assume request.user is the user to link.
|
||||
# In a real app, you might have more complex logic (e.g., admin creating for another user).
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
class PropertyViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = PropertySerializer
|
||||
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
search_fields = ['address', 'city', 'state', 'zip_code']
|
||||
filterset_fields = ['owner', 'state', 'city']
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.user_type == 'property_owner':
|
||||
return Property.objects.filter(owner__user=user)
|
||||
return Property.objects.all()
|
||||
|
||||
if user.user_type == "attorney":
|
||||
if not Attorney.objects.filter(user=user).exists():
|
||||
return Attorney.objects.create(user=user)
|
||||
|
||||
return Attorney.objects.filter(user=user)
|
||||
else:
|
||||
return Attorney.objects.all()
|
||||
|
||||
# Real Estate Agent ViewSet
|
||||
class RealEstateAgentViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = RealEstateAgentSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if self.request.user.user_type == 'property_owner':
|
||||
# Link to the currently authenticated user
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.user_type == "real_estate_agent":
|
||||
if not RealEstateAgent.objects.filter(user=user).exists():
|
||||
return RealEstateAgent.objects.create(user=user)
|
||||
return RealEstateAgent.objects.filter(user=user)
|
||||
else:
|
||||
return RealEstateAgent.objects.all()
|
||||
|
||||
|
||||
class PropertyViewSet(viewsets.ModelViewSet):
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
filterset_class = PropertyFilterSet
|
||||
search_fields = ["address", "city", "state", "zip_code"]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['increment_view_count', 'increment_save_count']:
|
||||
permission_classes = [IsAuthenticated]
|
||||
else:
|
||||
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Returns the serializer class to use depending on the action.
|
||||
- For 'list' and 'retrieve' (read operations), use PropertyResponseSerializer.
|
||||
- For 'create', 'update', 'partial_update' (write operations), use PropertyRequestSerializer.
|
||||
"""
|
||||
if self.action in ["list", "retrieve"]:
|
||||
return PropertyResponseSerializer
|
||||
return PropertyRequestSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
is_searching_others = bool(
|
||||
self.request.query_params.get(filters.SearchFilter.search_param)
|
||||
)
|
||||
if user.user_type == "property_owner":
|
||||
if is_searching_others:
|
||||
return Property.objects.exclude(owner__user=user)
|
||||
|
||||
else:
|
||||
return Property.objects.filter(owner__user=user)
|
||||
|
||||
return Property.objects.all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
|
||||
if self.request.user.user_type == "property_owner":
|
||||
owner = PropertyOwner.objects.get(user=self.request.user)
|
||||
serializer.save(owner=owner)
|
||||
## attempt to get the walkscore
|
||||
res = requests.get(f'https://api.walkscore.com/score?format=json&address={self.request.data['address']}&lat={self.request.data['latitude']}&lon={self.request.data['longitude']}&transit=1&bike=1&wsapikey={'4430c9adf62a4d93cd1efbdcd018b4d4'}')
|
||||
if res.ok:
|
||||
data = res.json()
|
||||
has_transit = data.get('transit')
|
||||
has_bike = data.get('bike')
|
||||
walk_score = PropertyWalkScoreInfo.objects.create(
|
||||
walk_score = data.get('walkscore'),
|
||||
walk_description = data.get('description'),
|
||||
ws_link = data.get('ws_link'),
|
||||
logo_url = data.get('logo_url'),
|
||||
transit_score = data.get('transit').get('score') if has_transit else None,
|
||||
transit_description = data.get('transit').get('description') if has_transit else None,
|
||||
transit_summary = data.get('transit').get('summary') if has_transit else None,
|
||||
bike_score = data.get('bike').get('score') if has_bike else None,
|
||||
bike_description = data.get('bike').get('description') if has_bike else None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
serializer.save(owner=owner, walk_score=walk_score)
|
||||
else:
|
||||
serializer.save(owner=owner)
|
||||
else:
|
||||
serializer.save()
|
||||
@action(detail=True, methods=['post'])
|
||||
def increment_view_count(self, request, pk=None):
|
||||
property_obj = self.get_object()
|
||||
property_obj.views += 1
|
||||
property_obj.save()
|
||||
# Create the user view model
|
||||
# UserViewModel.objects.create(
|
||||
# user__id=pk
|
||||
# )
|
||||
return Response({'views': property_obj.views}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def increment_save_count(self, request, pk=None):
|
||||
property_obj = self.get_object()
|
||||
property_obj.saves += 1
|
||||
property_obj.save()
|
||||
return Response({'saves': property_obj.saves}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class PropertyPictureViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||
serializer_class = PropertyPictureSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
|
||||
class PropertyDescriptionView(generics.UpdateAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def put(self, request, property_id):
|
||||
# check to make sure the property belongs to the user
|
||||
properties = Property.objects.filter(owner__user=request.user, id=property_id)
|
||||
if len(properties) == 0:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
elif len([properties]) > 1:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
# generate the description
|
||||
prop = properties.first()
|
||||
generator = PropertyDescriptionGenerator()
|
||||
description = generator.generate_response(prop)
|
||||
print(description)
|
||||
# save the description
|
||||
prop.description = description
|
||||
prop.save()
|
||||
serializer = PropertyResponseSerializer(prop)
|
||||
|
||||
# return the description
|
||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||
|
||||
|
||||
class VideoCategoryViewSet(viewsets.ModelViewSet):
|
||||
queryset = VideoCategory.objects.all()
|
||||
serializer_class = VideoCategorySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
class VideoViewSet(viewsets.ModelViewSet):
|
||||
queryset = Video.objects.all()
|
||||
serializer_class = VideoSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
search_fields = ['title', 'description']
|
||||
filterset_fields = ['category']
|
||||
search_fields = ["title", "description"]
|
||||
filterset_fields = ["category"]
|
||||
|
||||
|
||||
class UserVideoProgressViewSet(viewsets.ModelViewSet):
|
||||
queryset = UserVideoProgress.objects.all()
|
||||
|
||||
serializer_class = UserVideoProgressSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
def get_queryset(self):
|
||||
# first make sure that there is a progress for each video
|
||||
videos = Video.objects.all()
|
||||
for video in videos:
|
||||
UserVideoProgress.objects.get_or_create(
|
||||
user=self.request.user,
|
||||
video=video,
|
||||
)
|
||||
return UserVideoProgress.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class ConversationViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ConversationSerializer
|
||||
|
||||
permission_classes = [IsAuthenticated, IsParticipant]
|
||||
|
||||
search_fields = ["vendor", "property_owner"]
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Returns the serializer class to use depending on the action.
|
||||
- For 'list' and 'retrieve' (read operations), use PropertyResponseSerializer.
|
||||
- For 'create', 'update', 'partial_update' (write operations), use PropertyRequestSerializer.
|
||||
"""
|
||||
if self.action in ["list", "retrieve"]:
|
||||
return ConversationResponseSerializer
|
||||
return ConversationRequestSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.user_type == 'property_owner':
|
||||
if user.user_type == "property_owner":
|
||||
owner = PropertyOwner.objects.get(user=user)
|
||||
return Conversation.objects.filter(property_owner=owner)
|
||||
elif user.user_type == 'vendor':
|
||||
vendor_id = self.request.query_params.get("vendor")
|
||||
if vendor_id:
|
||||
return Conversation.objects.filter(
|
||||
property_owner=owner, vendor=vendor_id
|
||||
)
|
||||
else:
|
||||
return Conversation.objects.filter(property_owner=owner)
|
||||
elif user.user_type == "vendor":
|
||||
vendor = Vendor.objects.get(user=user)
|
||||
return Conversation.objects.filter(vendor=vendor)
|
||||
return Conversation.objects.none()
|
||||
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if self.request.user.user_type == 'property_owner':
|
||||
if self.request.user.user_type == "property_owner":
|
||||
owner = PropertyOwner.objects.get(user=self.request.user)
|
||||
serializer.save(property_owner=owner)
|
||||
elif self.request.user.user_type == 'vendor':
|
||||
elif self.request.user.user_type == "vendor":
|
||||
vendor = Vendor.objects.get(user=self.request.user)
|
||||
serializer.save(vendor=vendor)
|
||||
|
||||
|
||||
|
||||
|
||||
class MessageViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = MessageSerializer
|
||||
permission_classes = [IsAuthenticated, IsParticipant]
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
conversation_id = self.kwargs.get('conversation_id')
|
||||
conversation_id = self.kwargs.get("conversation_id")
|
||||
conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||
self.check_object_permissions(self.request, conversation)
|
||||
return Message.objects.filter(conversation=conversation).order_by('timestamp')
|
||||
|
||||
return Message.objects.filter(conversation=conversation).order_by("timestamp")
|
||||
|
||||
def perform_create(self, serializer):
|
||||
conversation_id = self.kwargs.get('conversation_id')
|
||||
conversation_id = self.kwargs.get("conversation_id")
|
||||
conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||
self.check_object_permissions(self.request, conversation)
|
||||
serializer.save(conversation=conversation, sender=self.request.user)
|
||||
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context['conversation_id'] = self.kwargs.get('conversation_id')
|
||||
return context
|
||||
context["conversation_id"] = self.kwargs.get("conversation_id")
|
||||
return context
|
||||
|
||||
|
||||
class OfferViewSet(viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [IsAuthenticated, IsParticipantInOffer]
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Returns the serializer class to use depending on the action.
|
||||
- For 'list' and 'retrieve' (read operations), use PropertyResponseSerializer.
|
||||
- For 'create', 'update', 'partial_update' (write operations), use PropertyRequestSerializer.
|
||||
"""
|
||||
if self.action in ["list", "retrieve"]:
|
||||
return OfferResponseSerializer
|
||||
return OfferRequestSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user_lookup = Q(user=self.request.user)
|
||||
property_lookup = Q(property__owner__user=self.request.user)
|
||||
null_previous_offer = Q(previous_offer=None)
|
||||
return Offer.objects.filter(
|
||||
(property_lookup & null_previous_offer)
|
||||
| (user_lookup & null_previous_offer)
|
||||
)
|
||||
|
||||
class BidViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.user_type == 'property_owner':
|
||||
return Bid.objects.filter(property__owner__user=user).order_by('-created_at')
|
||||
elif user.user_type == 'vendor':
|
||||
# Vendors should see all bids, but only their own responses
|
||||
return Bid.objects.all().order_by('-created_at')
|
||||
return Bid.objects.none()
|
||||
|
||||
def get_serializer_class(self):
|
||||
return BidSerializer
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def select_response(self, request, pk=None):
|
||||
bid = self.get_object()
|
||||
response_id = request.data.get('response_id')
|
||||
try:
|
||||
response = BidResponse.objects.get(id=response_id, bid=bid)
|
||||
# Ensure the current user is the property owner of the bid
|
||||
if request.user == bid.property.owner.user:
|
||||
# Unselect any previously selected response for this bid
|
||||
BidResponse.objects.filter(bid=bid, status='selected').update(status='submitted')
|
||||
# Select the new response
|
||||
response.status = 'selected'
|
||||
response.save()
|
||||
return Response({'status': 'response selected'})
|
||||
return Response({'error': 'You do not have permission to perform this action.'}, status=403)
|
||||
except BidResponse.DoesNotExist:
|
||||
return Response({'error': 'Response not found.'}, status=404)
|
||||
|
||||
class BidResponseViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = BidResponseSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.user_type == 'property_owner':
|
||||
return BidResponse.objects.filter(bid__property__owner__user=user).order_by('-created_at')
|
||||
elif user.user_type == 'vendor':
|
||||
return BidResponse.objects.filter(vendor__user=user).order_by('-created_at')
|
||||
return BidResponse.objects.none()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# A vendor can only create one response per bid
|
||||
bid = serializer.validated_data['bid']
|
||||
vendor = self.request.user.vendor
|
||||
if BidResponse.objects.filter(bid=bid, vendor=vendor).exists():
|
||||
raise serializers.ValidationError("You have already responded to this bid.")
|
||||
serializer.save(vendor=vendor, status='submitted')
|
||||
|
||||
|
||||
class PropertySaveViewSet(
|
||||
viewsets.mixins.CreateModelMixin,
|
||||
viewsets.mixins.ListModelMixin,
|
||||
viewsets.mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet
|
||||
):
|
||||
"""
|
||||
A viewset that provides 'create', 'list', and 'destroy' actions
|
||||
for saved properties.
|
||||
"""
|
||||
queryset = PropertySave.objects.all()
|
||||
serializer_class = PropertySaveSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
This view should return a list of all saved properties
|
||||
for the currently authenticated user.
|
||||
"""
|
||||
user = self.request.user
|
||||
return PropertySave.objects.filter(user=user).order_by('-created_at')
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Saves the new PropertySave instance, associating it with
|
||||
the current authenticated user.
|
||||
"""
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""
|
||||
Unsaves a property.
|
||||
"""
|
||||
try:
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
return Response(
|
||||
{"detail": "Property successfully unsaved."},
|
||||
status=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
except PropertySave.DoesNotExist:
|
||||
return Response(
|
||||
{"detail": "PropertySave instance not found."},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@@ -8,17 +8,21 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django.setup()
|
||||
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
import core.routing
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": get_asgi_application(),
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
core.routing.websocket_urlpatterns
|
||||
)
|
||||
),
|
||||
})
|
||||
URLRouter(core.routing.websocket_urlpatterns)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -31,29 +31,41 @@ DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = False
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://127.0.0.1", "http://localhost:3000"]
|
||||
ALLOWED_HOSTS = [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"localhost:3000",
|
||||
"127.0.0.1:3000",
|
||||
"10.0.0.9 njm:3000",
|
||||
]
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"daphne",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
|
||||
# Third-party apps
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'rest_framework_simplejwt.token_blacklist',
|
||||
'channels',
|
||||
'django_filters',
|
||||
|
||||
|
||||
# Local apps
|
||||
'core',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
@@ -82,7 +94,7 @@ TEMPLATES = [
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'dta_service.wsgi.application'
|
||||
ASGI_APPLICATION = 'config.asgi.application'
|
||||
ASGI_APPLICATION = 'dta_service.asgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
@@ -152,6 +164,10 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
X_FRAME_OPTIONS = 'ALLOW-FROM 127.0.0.1:8010/'
|
||||
|
||||
|
||||
# JWT Settings
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
|
||||
@@ -168,8 +184,7 @@ SIMPLE_JWT = {
|
||||
'JWK_URL': None,
|
||||
'LEEWAY': 0,
|
||||
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
|
||||
"AUTH_HEADER_TYPES": ("JWT",),
|
||||
'USER_ID_FIELD': 'id',
|
||||
'USER_ID_CLAIM': 'user_id',
|
||||
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
|
||||
@@ -209,4 +224,8 @@ SIMPLE_JWT = {
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
TEST_DISCOVER_PATTERN = "test_*.py"
|
||||
TEST_DISCOVER_PATTERN = "test_*.py"
|
||||
|
||||
|
||||
# Realestate api
|
||||
REAL_ESTATE_API_KEY = "AIMLOPERATIONSLLC-a7ce-7525-b38f-cf8b4d52ca70"
|
||||
|
||||
@@ -20,22 +20,16 @@ from rest_framework_simplejwt.views import (
|
||||
TokenRefreshView,
|
||||
)
|
||||
from core.views import (
|
||||
CustomTokenObtainPairView, LogoutView,
|
||||
CustomTokenObtainPairView, LogoutView,
|
||||
PasswordResetRequestView, PasswordResetConfirmView,
|
||||
UserRegisterView
|
||||
UserRegisterView, UserRetrieveView, UserSignTosView, PropertyDescriptionView
|
||||
)
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from core.views import VideoCategoryViewSet, VideoViewSet, UserVideoProgressViewSet, VendorViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'categories', VideoCategoryViewSet, basename='video-category')
|
||||
router.register(r'', VideoViewSet, basename='video')
|
||||
router.register(r'progress', UserVideoProgressViewSet, basename='video-progress')
|
||||
router.register(r'', VendorViewSet, basename='vendor')
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
|
||||
# Authentication
|
||||
path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
@@ -43,11 +37,31 @@ urlpatterns = [
|
||||
path('api/register/', UserRegisterView.as_view(), name='register'),
|
||||
path('api/password-reset/', PasswordResetRequestView.as_view(), name='password_reset'),
|
||||
path('api/password-reset/confirm/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
|
||||
|
||||
|
||||
# API endpoints
|
||||
path('api/user/', UserRetrieveView.as_view(), name='get_user'),
|
||||
path('api/user/acknowledge_tos/', UserSignTosView.as_view(), name='sign_tos'),
|
||||
path('api/property-description-generator/<int:property_id>/', PropertyDescriptionView.as_view(), name='property-description-generator'),
|
||||
path('api/property-owners/', include('core.urls.property_owner')),
|
||||
path('api/vendors/', include('core.urls.vendor')),
|
||||
path('api/properties/', include('core.urls.property')),
|
||||
path('api/videos/', include('core.urls.video')),
|
||||
path('api/conversations/', include('core.urls.conversation')),
|
||||
]
|
||||
path('api/offers/', include('core.urls.offer')),
|
||||
path('api/attorney/', include('core.urls.attorney')),
|
||||
path('api/real_estate_agent/', include('core.urls.real_estate_agent')),
|
||||
path('api/saved-properties/', include('core.urls.property_save')),
|
||||
path('api/', include('core.urls.bid')),
|
||||
]
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
def print_patterns(patterns, base_path=""):
|
||||
for pattern in patterns:
|
||||
if hasattr(pattern, 'url_patterns'): # It's a URLResolver
|
||||
print_patterns(pattern.url_patterns, base_path + str(pattern.pattern))
|
||||
else: # It's a URLPattern
|
||||
full_path = base_path + str(pattern.pattern)
|
||||
view_name = pattern.callback.__module__ + "." + pattern.callback.__name__
|
||||
print(f"URL: /{full_path.replace('^', '').replace('$', '')} -> View: {view_name}")
|
||||
|
||||
print_patterns(urlpatterns)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# core/tests/__init__.py
|
||||
from .test_models import *
|
||||
from .test_serializers import *
|
||||
from .test_views import *
|
||||
from .test_views import *
|
||||
|
||||
@@ -1,328 +1,337 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from core.models import (
|
||||
PropertyOwner, Vendor, Property, VideoCategory, Video,
|
||||
UserVideoProgress, Conversation, Message, PasswordResetToken
|
||||
PropertyOwner,
|
||||
Vendor,
|
||||
Property,
|
||||
VideoCategory,
|
||||
Video,
|
||||
UserVideoProgress,
|
||||
Conversation,
|
||||
Message,
|
||||
PasswordResetToken,
|
||||
)
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='test@example.com',
|
||||
first_name='Test',
|
||||
last_name='User',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
|
||||
|
||||
def test_user_creation(self):
|
||||
self.assertEqual(self.user.email, 'test@example.com')
|
||||
self.assertEqual(self.user.first_name, 'Test')
|
||||
self.assertEqual(self.user.last_name, 'User')
|
||||
self.assertEqual(self.user.user_type, 'property_owner')
|
||||
self.assertTrue(self.user.check_password('testpass123'))
|
||||
|
||||
self.assertEqual(self.user.email, "test@example.com")
|
||||
self.assertEqual(self.user.first_name, "Test")
|
||||
self.assertEqual(self.user.last_name, "User")
|
||||
self.assertEqual(self.user.user_type, "property_owner")
|
||||
self.assertTrue(self.user.check_password("testpass123"))
|
||||
|
||||
def test_user_str(self):
|
||||
self.assertEqual(str(self.user), 'test@example.com')
|
||||
self.assertEqual(str(self.user), "test@example.com")
|
||||
|
||||
|
||||
class PropertyOwnerModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='owner@example.com',
|
||||
first_name='Property',
|
||||
last_name='Owner',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(
|
||||
user=self.user,
|
||||
phone_number='1234567890'
|
||||
user=self.user, phone_number="1234567890"
|
||||
)
|
||||
|
||||
|
||||
def test_property_owner_creation(self):
|
||||
self.assertEqual(self.owner.user, self.user)
|
||||
self.assertEqual(self.owner.phone_number, '1234567890')
|
||||
|
||||
self.assertEqual(self.owner.phone_number, "1234567890")
|
||||
|
||||
def test_property_owner_str(self):
|
||||
self.assertEqual(str(self.owner), 'Property Owner')
|
||||
self.assertEqual(str(self.owner), "Property Owner")
|
||||
|
||||
|
||||
class VendorModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='vendor@example.com',
|
||||
first_name='Vendor',
|
||||
last_name='User',
|
||||
user_type='vendor',
|
||||
password='testpass123'
|
||||
email="vendor@example.com",
|
||||
first_name="Vendor",
|
||||
last_name="User",
|
||||
user_type="vendor",
|
||||
password="testpass123",
|
||||
)
|
||||
self.vendor = Vendor.objects.create(
|
||||
user=self.user,
|
||||
business_name='Test Vendor',
|
||||
business_type='contractor',
|
||||
phone_number='1234567890',
|
||||
address='123 Test St',
|
||||
city='Testville',
|
||||
state='TS',
|
||||
zip_code='12345'
|
||||
business_name="Test Vendor",
|
||||
business_type="contractor",
|
||||
phone_number="1234567890",
|
||||
address="123 Test St",
|
||||
city="Testville",
|
||||
state="TS",
|
||||
zip_code="12345",
|
||||
)
|
||||
|
||||
|
||||
def test_vendor_creation(self):
|
||||
self.assertEqual(self.vendor.user, self.user)
|
||||
self.assertEqual(self.vendor.business_name, 'Test Vendor')
|
||||
self.assertEqual(self.vendor.business_type, 'contractor')
|
||||
self.assertEqual(self.vendor.phone_number, '1234567890')
|
||||
self.assertEqual(self.vendor.address, '123 Test St')
|
||||
self.assertEqual(self.vendor.city, 'Testville')
|
||||
self.assertEqual(self.vendor.state, 'TS')
|
||||
self.assertEqual(self.vendor.zip_code, '12345')
|
||||
|
||||
self.assertEqual(self.vendor.business_name, "Test Vendor")
|
||||
self.assertEqual(self.vendor.business_type, "contractor")
|
||||
self.assertEqual(self.vendor.phone_number, "1234567890")
|
||||
self.assertEqual(self.vendor.address, "123 Test St")
|
||||
self.assertEqual(self.vendor.city, "Testville")
|
||||
self.assertEqual(self.vendor.state, "TS")
|
||||
self.assertEqual(self.vendor.zip_code, "12345")
|
||||
|
||||
def test_vendor_str(self):
|
||||
self.assertEqual(str(self.vendor), 'Test Vendor')
|
||||
self.assertEqual(str(self.vendor), "Test Vendor")
|
||||
|
||||
|
||||
class PropertyModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='owner@example.com',
|
||||
first_name='Property',
|
||||
last_name='Owner',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(user=self.user)
|
||||
self.property = Property.objects.create(
|
||||
owner=self.owner,
|
||||
address='123 Main St',
|
||||
city='Anytown',
|
||||
state='CA',
|
||||
zip_code='90210',
|
||||
address="123 Main St",
|
||||
city="Anytown",
|
||||
state="CA",
|
||||
zip_code="90210",
|
||||
market_value=500000.00,
|
||||
loan_amount=400000.00,
|
||||
loan_interest_rate=3.5,
|
||||
loan_term=30,
|
||||
loan_start_date='2020-01-01'
|
||||
loan_start_date="2020-01-01",
|
||||
)
|
||||
|
||||
|
||||
def test_property_creation(self):
|
||||
self.assertEqual(self.property.owner, self.owner)
|
||||
self.assertEqual(self.property.address, '123 Main St')
|
||||
self.assertEqual(self.property.city, 'Anytown')
|
||||
self.assertEqual(self.property.state, 'CA')
|
||||
self.assertEqual(self.property.zip_code, '90210')
|
||||
self.assertEqual(self.property.address, "123 Main St")
|
||||
self.assertEqual(self.property.city, "Anytown")
|
||||
self.assertEqual(self.property.state, "CA")
|
||||
self.assertEqual(self.property.zip_code, "90210")
|
||||
self.assertEqual(self.property.market_value, 500000.00)
|
||||
self.assertEqual(self.property.loan_amount, 400000.00)
|
||||
self.assertEqual(self.property.loan_interest_rate, 3.5)
|
||||
self.assertEqual(self.property.loan_term, 30)
|
||||
self.assertEqual(str(self.property.loan_start_date), '2020-01-01')
|
||||
|
||||
self.assertEqual(str(self.property.loan_start_date), "2020-01-01")
|
||||
|
||||
def test_property_str(self):
|
||||
self.assertEqual(str(self.property), '123 Main St, Anytown, CA 90210')
|
||||
self.assertEqual(str(self.property), "123 Main St, Anytown, CA 90210")
|
||||
|
||||
|
||||
class VideoModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.category = VideoCategory.objects.create(
|
||||
name='Test Category',
|
||||
description='Test Description'
|
||||
name="Test Category", description="Test Description"
|
||||
)
|
||||
self.video = Video.objects.create(
|
||||
category=self.category,
|
||||
title='Test Video',
|
||||
description='Test Video Description',
|
||||
link='https://example.com/video',
|
||||
duration=300
|
||||
title="Test Video",
|
||||
description="Test Video Description",
|
||||
link="https://example.com/video",
|
||||
duration=300,
|
||||
)
|
||||
|
||||
|
||||
def test_video_creation(self):
|
||||
self.assertEqual(self.video.category, self.category)
|
||||
self.assertEqual(self.video.title, 'Test Video')
|
||||
self.assertEqual(self.video.description, 'Test Video Description')
|
||||
self.assertEqual(self.video.link, 'https://example.com/video')
|
||||
self.assertEqual(self.video.title, "Test Video")
|
||||
self.assertEqual(self.video.description, "Test Video Description")
|
||||
self.assertEqual(self.video.link, "https://example.com/video")
|
||||
self.assertEqual(self.video.duration, 300)
|
||||
|
||||
|
||||
def test_video_str(self):
|
||||
self.assertEqual(str(self.video), 'Test Video')
|
||||
self.assertEqual(str(self.video), "Test Video")
|
||||
|
||||
|
||||
class UserVideoProgressModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='user@example.com',
|
||||
first_name='Test',
|
||||
last_name='User',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="user@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.category = VideoCategory.objects.create(name='Test Category')
|
||||
self.category = VideoCategory.objects.create(name="Test Category")
|
||||
self.video = Video.objects.create(
|
||||
category=self.category,
|
||||
title='Test Video',
|
||||
link='https://example.com/video',
|
||||
duration=300
|
||||
title="Test Video",
|
||||
link="https://example.com/video",
|
||||
duration=300,
|
||||
)
|
||||
self.progress = UserVideoProgress.objects.create(
|
||||
user=self.user,
|
||||
video=self.video,
|
||||
progress=150
|
||||
user=self.user, video=self.video, progress=150
|
||||
)
|
||||
|
||||
|
||||
def test_progress_creation(self):
|
||||
self.assertEqual(self.progress.user, self.user)
|
||||
self.assertEqual(self.progress.video, self.video)
|
||||
self.assertEqual(self.progress.progress, 150)
|
||||
self.assertEqual(self.progress.status, 'in_progress')
|
||||
|
||||
self.assertEqual(self.progress.status, "in_progress")
|
||||
|
||||
def test_status_update(self):
|
||||
# Test not started
|
||||
self.progress.progress = 0
|
||||
self.progress.save()
|
||||
self.assertEqual(self.progress.status, 'not_started')
|
||||
|
||||
self.assertEqual(self.progress.status, "not_started")
|
||||
|
||||
# Test completed
|
||||
self.progress.progress = 300
|
||||
self.progress.save()
|
||||
self.assertEqual(self.progress.status, 'completed')
|
||||
|
||||
self.assertEqual(self.progress.status, "completed")
|
||||
|
||||
# Test in progress
|
||||
self.progress.progress = 150
|
||||
self.progress.save()
|
||||
self.assertEqual(self.progress.status, 'in_progress')
|
||||
|
||||
self.assertEqual(self.progress.status, "in_progress")
|
||||
|
||||
def test_progress_str(self):
|
||||
self.assertEqual(str(self.progress), 'user@example.com - Test Video - in_progress')
|
||||
self.assertEqual(
|
||||
str(self.progress), "user@example.com - Test Video - in_progress"
|
||||
)
|
||||
|
||||
|
||||
class ConversationModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner_user = User.objects.create_user(
|
||||
email='owner@example.com',
|
||||
first_name='Property',
|
||||
last_name='Owner',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(user=self.owner_user)
|
||||
|
||||
|
||||
self.vendor_user = User.objects.create_user(
|
||||
email='vendor@example.com',
|
||||
first_name='Vendor',
|
||||
last_name='User',
|
||||
user_type='vendor',
|
||||
password='testpass123'
|
||||
email="vendor@example.com",
|
||||
first_name="Vendor",
|
||||
last_name="User",
|
||||
user_type="vendor",
|
||||
password="testpass123",
|
||||
)
|
||||
self.vendor = Vendor.objects.create(
|
||||
user=self.vendor_user,
|
||||
business_name='Test Vendor',
|
||||
business_type='contractor'
|
||||
business_name="Test Vendor",
|
||||
business_type="contractor",
|
||||
)
|
||||
|
||||
|
||||
self.property = Property.objects.create(
|
||||
owner=self.owner,
|
||||
address='123 Main St',
|
||||
city='Anytown',
|
||||
state='CA',
|
||||
zip_code='90210',
|
||||
market_value=500000.00
|
||||
address="123 Main St",
|
||||
city="Anytown",
|
||||
state="CA",
|
||||
zip_code="90210",
|
||||
market_value=500000.00,
|
||||
)
|
||||
|
||||
|
||||
self.conversation = Conversation.objects.create(
|
||||
property_owner=self.owner,
|
||||
vendor=self.vendor,
|
||||
property=self.property
|
||||
property_owner=self.owner, vendor=self.vendor, property=self.property
|
||||
)
|
||||
|
||||
|
||||
def test_conversation_creation(self):
|
||||
self.assertEqual(self.conversation.property_owner, self.owner)
|
||||
self.assertEqual(self.conversation.vendor, self.vendor)
|
||||
self.assertEqual(self.conversation.property, self.property)
|
||||
|
||||
|
||||
def test_conversation_str(self):
|
||||
expected_str = f"Conversation between {self.owner} and {self.vendor} about {self.property}"
|
||||
expected_str = (
|
||||
f"Conversation between {self.owner} and {self.vendor} about {self.property}"
|
||||
)
|
||||
self.assertEqual(str(self.conversation), expected_str)
|
||||
|
||||
|
||||
class MessageModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner_user = User.objects.create_user(
|
||||
email='owner@example.com',
|
||||
first_name='Property',
|
||||
last_name='Owner',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(user=self.owner_user)
|
||||
|
||||
|
||||
self.vendor_user = User.objects.create_user(
|
||||
email='vendor@example.com',
|
||||
first_name='Vendor',
|
||||
last_name='User',
|
||||
user_type='vendor',
|
||||
password='testpass123'
|
||||
email="vendor@example.com",
|
||||
first_name="Vendor",
|
||||
last_name="User",
|
||||
user_type="vendor",
|
||||
password="testpass123",
|
||||
)
|
||||
self.vendor = Vendor.objects.create(
|
||||
user=self.vendor_user,
|
||||
business_name='Test Vendor',
|
||||
business_type='contractor'
|
||||
business_name="Test Vendor",
|
||||
business_type="contractor",
|
||||
)
|
||||
|
||||
|
||||
self.property = Property.objects.create(
|
||||
owner=self.owner,
|
||||
address='123 Main St',
|
||||
city='Anytown',
|
||||
state='CA',
|
||||
zip_code='90210',
|
||||
market_value=500000.00
|
||||
address="123 Main St",
|
||||
city="Anytown",
|
||||
state="CA",
|
||||
zip_code="90210",
|
||||
market_value=500000.00,
|
||||
)
|
||||
|
||||
|
||||
self.conversation = Conversation.objects.create(
|
||||
property_owner=self.owner,
|
||||
vendor=self.vendor,
|
||||
property=self.property
|
||||
property_owner=self.owner, vendor=self.vendor, property=self.property
|
||||
)
|
||||
|
||||
|
||||
self.message = Message.objects.create(
|
||||
conversation=self.conversation,
|
||||
sender=self.owner_user,
|
||||
text='Test message'
|
||||
conversation=self.conversation, sender=self.owner_user, text="Test message"
|
||||
)
|
||||
|
||||
|
||||
def test_message_creation(self):
|
||||
self.assertEqual(self.message.conversation, self.conversation)
|
||||
self.assertEqual(self.message.sender, self.owner_user)
|
||||
self.assertEqual(self.message.text, 'Test message')
|
||||
self.assertEqual(self.message.text, "Test message")
|
||||
self.assertFalse(self.message.read)
|
||||
|
||||
|
||||
def test_message_str(self):
|
||||
expected_str = f"Message from {self.owner_user} in {self.conversation}"
|
||||
self.assertEqual(str(self.message), expected_str)
|
||||
|
||||
|
||||
class PasswordResetTokenModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='user@example.com',
|
||||
first_name='Test',
|
||||
last_name='User',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="user@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.token = PasswordResetToken.objects.create(
|
||||
user=self.user,
|
||||
expires_at=timezone.now() + timedelta(hours=24)
|
||||
user=self.user, expires_at=timezone.now() + timedelta(hours=24)
|
||||
)
|
||||
|
||||
|
||||
def test_token_creation(self):
|
||||
self.assertEqual(self.token.user, self.user)
|
||||
self.assertFalse(self.token.used)
|
||||
self.assertTrue(self.token.is_valid())
|
||||
|
||||
|
||||
def test_token_invalid_after_use(self):
|
||||
self.token.used = True
|
||||
self.token.save()
|
||||
self.assertFalse(self.token.is_valid())
|
||||
|
||||
|
||||
def test_token_invalid_after_expiry(self):
|
||||
self.token.expires_at = timezone.now() - timedelta(hours=1)
|
||||
self.token.save()
|
||||
self.assertFalse(self.token.is_valid())
|
||||
|
||||
|
||||
def test_token_str(self):
|
||||
self.assertEqual(str(self.token), f"Password reset token for {self.user.email}")
|
||||
self.assertEqual(str(self.token), f"Password reset token for {self.user.email}")
|
||||
|
||||
@@ -2,510 +2,710 @@ from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from core.serializers import (
|
||||
UserRegisterSerializer, PropertyOwnerSerializer, VendorSerializer,
|
||||
PropertySerializer, VideoSerializer, UserVideoProgressSerializer,
|
||||
ConversationSerializer, MessageSerializer, PasswordResetRequestSerializer,
|
||||
PasswordResetConfirmSerializer
|
||||
UserRegisterSerializer,
|
||||
PropertyOwnerSerializer,
|
||||
VendorSerializer,
|
||||
PropertyRequestSerializer,
|
||||
VideoSerializer,
|
||||
UserVideoProgressSerializer,
|
||||
ConversationRequestSerializer,
|
||||
ConversationResponseSerializer,
|
||||
MessageSerializer,
|
||||
PasswordResetRequestSerializer,
|
||||
PasswordResetConfirmSerializer,
|
||||
OfferResponseSerializer,
|
||||
OfferRequestSerializer,
|
||||
)
|
||||
from core.models import (
|
||||
PropertyOwner, Vendor, Property, VideoCategory, Video,
|
||||
UserVideoProgress, Conversation, Message, PasswordResetToken
|
||||
PropertyOwner,
|
||||
Vendor,
|
||||
Property,
|
||||
VideoCategory,
|
||||
Video,
|
||||
UserVideoProgress,
|
||||
Conversation,
|
||||
Message,
|
||||
PasswordResetToken,
|
||||
Offer,
|
||||
)
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserRegisterSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.valid_data = {
|
||||
'email': 'test@example.com',
|
||||
'first_name': 'Test',
|
||||
'last_name': 'User',
|
||||
'user_type': 'property_owner',
|
||||
'password': 'testpass123',
|
||||
'password2': 'testpass123'
|
||||
"email": "test@example.com",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
"user_type": "property_owner",
|
||||
"password": "testpass123",
|
||||
"password2": "testpass123",
|
||||
}
|
||||
|
||||
|
||||
def test_valid_serializer(self):
|
||||
serializer = UserRegisterSerializer(data=self.valid_data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
user = serializer.save()
|
||||
self.assertEqual(user.email, 'test@example.com')
|
||||
self.assertEqual(user.first_name, 'Test')
|
||||
self.assertEqual(user.last_name, 'User')
|
||||
self.assertEqual(user.user_type, 'property_owner')
|
||||
|
||||
self.assertEqual(user.email, "test@example.com")
|
||||
self.assertEqual(user.first_name, "Test")
|
||||
self.assertEqual(user.last_name, "User")
|
||||
self.assertEqual(user.user_type, "property_owner")
|
||||
|
||||
def test_password_mismatch(self):
|
||||
invalid_data = self.valid_data.copy()
|
||||
invalid_data['password2'] = 'differentpass'
|
||||
invalid_data["password2"] = "differentpass"
|
||||
serializer = UserRegisterSerializer(data=invalid_data)
|
||||
with self.assertRaises(ValidationError):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
||||
class PropertyOwnerSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user_data = {
|
||||
'email': 'owner@example.com',
|
||||
'first_name': 'Property',
|
||||
'last_name': 'Owner',
|
||||
'user_type': 'property_owner',
|
||||
'password': 'testpass123'
|
||||
"email": "owner@example.com",
|
||||
"first_name": "Property",
|
||||
"last_name": "Owner",
|
||||
"user_type": "property_owner",
|
||||
"password": "testpass123",
|
||||
}
|
||||
self.owner_data = {
|
||||
'user': self.user_data,
|
||||
'phone_number': '1234567890'
|
||||
}
|
||||
|
||||
self.owner_data = {"user": self.user_data, "phone_number": "1234567890"}
|
||||
|
||||
def test_create_property_owner(self):
|
||||
serializer = PropertyOwnerSerializer(data=self.owner_data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
owner = serializer.save()
|
||||
self.assertEqual(owner.user.email, 'owner@example.com')
|
||||
self.assertEqual(owner.phone_number, '1234567890')
|
||||
|
||||
self.assertEqual(owner.user.email, "owner@example.com")
|
||||
self.assertEqual(owner.phone_number, "1234567890")
|
||||
|
||||
def test_update_property_owner(self):
|
||||
user = User.objects.create_user(**self.user_data)
|
||||
owner = PropertyOwner.objects.create(user=user, phone_number='1234567890')
|
||||
|
||||
owner = PropertyOwner.objects.create(user=user, phone_number="1234567890")
|
||||
|
||||
update_data = {
|
||||
'user': {
|
||||
'first_name': 'NewName',
|
||||
'last_name': 'NewLast',
|
||||
'email': 'newowner@example.com'
|
||||
"user": {
|
||||
"first_name": "NewName",
|
||||
"last_name": "NewLast",
|
||||
"email": "newowner@example.com",
|
||||
},
|
||||
'phone_number': '9876543210'
|
||||
"phone_number": "9876543210",
|
||||
}
|
||||
|
||||
serializer = PropertyOwnerSerializer(instance=owner, data=update_data, partial=True)
|
||||
|
||||
serializer = PropertyOwnerSerializer(
|
||||
instance=owner, data=update_data, partial=True
|
||||
)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
updated_owner = serializer.save()
|
||||
|
||||
self.assertEqual(updated_owner.user.first_name, 'NewName')
|
||||
self.assertEqual(updated_owner.user.last_name, 'NewLast')
|
||||
self.assertEqual(updated_owner.phone_number, '9876543210')
|
||||
|
||||
self.assertEqual(updated_owner.user.first_name, "NewName")
|
||||
self.assertEqual(updated_owner.user.last_name, "NewLast")
|
||||
self.assertEqual(updated_owner.phone_number, "9876543210")
|
||||
|
||||
|
||||
class VendorSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user_data = {
|
||||
'email': 'vendor@example.com',
|
||||
'first_name': 'Vendor',
|
||||
'last_name': 'User',
|
||||
'user_type': 'vendor',
|
||||
'password': 'testpass123'
|
||||
"email": "vendor@example.com",
|
||||
"first_name": "Vendor",
|
||||
"last_name": "User",
|
||||
"user_type": "vendor",
|
||||
"password": "testpass123",
|
||||
}
|
||||
self.vendor_data = {
|
||||
'user': self.user_data,
|
||||
'business_name': 'Test Vendor',
|
||||
'business_type': 'contractor',
|
||||
'phone_number': '1234567890',
|
||||
'address': '123 Test St',
|
||||
'city': 'Testville',
|
||||
'state': 'TS',
|
||||
'zip_code': '12345'
|
||||
"user": self.user_data,
|
||||
"business_name": "Test Vendor",
|
||||
"business_type": "electrician",
|
||||
"phone_number": "1234567890",
|
||||
"address": "123 Test St",
|
||||
"city": "Testville",
|
||||
"state": "TS",
|
||||
"zip_code": "12345",
|
||||
}
|
||||
|
||||
|
||||
def test_create_vendor(self):
|
||||
serializer = VendorSerializer(data=self.vendor_data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
vendor = serializer.save()
|
||||
self.assertEqual(vendor.user.email, 'vendor@example.com')
|
||||
self.assertEqual(vendor.business_name, 'Test Vendor')
|
||||
self.assertEqual(vendor.business_type, 'contractor')
|
||||
self.assertEqual(vendor.phone_number, '1234567890')
|
||||
self.assertEqual(vendor.address, '123 Test St')
|
||||
self.assertEqual(vendor.city, 'Testville')
|
||||
self.assertEqual(vendor.state, 'TS')
|
||||
self.assertEqual(vendor.zip_code, '12345')
|
||||
|
||||
self.assertEqual(vendor.user.email, "vendor@example.com")
|
||||
self.assertEqual(vendor.business_name, "Test Vendor")
|
||||
self.assertEqual(vendor.business_type, "electrician")
|
||||
self.assertEqual(vendor.phone_number, "1234567890")
|
||||
self.assertEqual(vendor.address, "123 Test St")
|
||||
self.assertEqual(vendor.city, "Testville")
|
||||
self.assertEqual(vendor.state, "TS")
|
||||
self.assertEqual(vendor.zip_code, "12345")
|
||||
|
||||
def test_update_vendor(self):
|
||||
user = User.objects.create_user(**self.user_data)
|
||||
vendor = Vendor.objects.create(
|
||||
user=user,
|
||||
business_name='Test Vendor',
|
||||
business_type='contractor',
|
||||
phone_number='1234567890',
|
||||
address='123 Test St',
|
||||
city='Testville',
|
||||
state='TS',
|
||||
zip_code='12345'
|
||||
business_name="Test Vendor",
|
||||
business_type="electrician",
|
||||
phone_number="1234567890",
|
||||
address="123 Test St",
|
||||
city="Testville",
|
||||
state="TS",
|
||||
zip_code="12345",
|
||||
)
|
||||
|
||||
|
||||
update_data = {
|
||||
'user': {
|
||||
'first_name': 'NewVendor',
|
||||
'last_name': 'NewUser',
|
||||
'email': 'newvendor@example.com'
|
||||
"user": {
|
||||
"first_name": "NewVendor",
|
||||
"last_name": "NewUser",
|
||||
"email": "newvendor@example.com",
|
||||
},
|
||||
'business_name': 'Updated Vendor',
|
||||
'phone_number': '9876543210'
|
||||
"business_name": "Updated Vendor",
|
||||
"phone_number": "9876543210",
|
||||
}
|
||||
|
||||
|
||||
serializer = VendorSerializer(instance=vendor, data=update_data, partial=True)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
updated_vendor = serializer.save()
|
||||
|
||||
self.assertEqual(updated_vendor.user.first_name, 'NewVendor')
|
||||
self.assertEqual(updated_vendor.user.last_name, 'NewUser')
|
||||
self.assertEqual(updated_vendor.business_name, 'Updated Vendor')
|
||||
self.assertEqual(updated_vendor.phone_number, '9876543210')
|
||||
|
||||
self.assertEqual(updated_vendor.user.first_name, "NewVendor")
|
||||
self.assertEqual(updated_vendor.user.last_name, "NewUser")
|
||||
self.assertEqual(updated_vendor.business_name, "Updated Vendor")
|
||||
self.assertEqual(updated_vendor.phone_number, "9876543210")
|
||||
|
||||
|
||||
class PropertySerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='owner@example.com',
|
||||
first_name='Property',
|
||||
last_name='Owner',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(user=self.user)
|
||||
|
||||
self.property_data = {
|
||||
'owner': self.owner.pk,
|
||||
'address': '123 Main St',
|
||||
'city': 'Anytown',
|
||||
'state': 'CA',
|
||||
'zip_code': '90210',
|
||||
'market_value': '500000.00',
|
||||
'loan_amount': '400000.00',
|
||||
'loan_interest_rate': '3.50',
|
||||
'loan_term': 30,
|
||||
'loan_start_date': '2020-01-01'
|
||||
"owner": self.owner.pk,
|
||||
"address": "123 Main St",
|
||||
"city": "Anytown",
|
||||
"state": "CA",
|
||||
"zip_code": "90210",
|
||||
"market_value": "500000.00",
|
||||
"loan_amount": "400000.00",
|
||||
"loan_interest_rate": "3.50",
|
||||
"loan_term": 30,
|
||||
"loan_start_date": "2020-01-01",
|
||||
}
|
||||
|
||||
|
||||
self.full_property_data = {
|
||||
"owner": self.owner.pk,
|
||||
"address": "1968 Green Apple Ct, Orange Park, FL, 32073",
|
||||
"city": "Wheaton",
|
||||
"state": "IL",
|
||||
"zip_code": "60189",
|
||||
"latitude": 41.833230929728565,
|
||||
"longitude": -88.12083257242568,
|
||||
"market_value": "742000",
|
||||
"loan_amount": "449000",
|
||||
"loan_term": "360",
|
||||
"loan_start_date": "2021-01-05",
|
||||
"description": "",
|
||||
"features": [],
|
||||
"pictures": [],
|
||||
"num_bedrooms": None,
|
||||
"num_bathrooms": 3,
|
||||
"sq_ft": 2598,
|
||||
"realestate_api_id": 175468968,
|
||||
"views": 0,
|
||||
"saves": 0,
|
||||
"property_status": "off_market",
|
||||
"schools": [
|
||||
{
|
||||
"city": "Wheaton",
|
||||
"state": "IL",
|
||||
"zip_code": "60189",
|
||||
"latitude": 41.834869,
|
||||
"longitude": -88.146118,
|
||||
"school_type": "Public",
|
||||
"enrollment": 1982,
|
||||
"grades": "9-12",
|
||||
"name": "Wheaton Warrenville South High School",
|
||||
"parent_rating": 3,
|
||||
"rating": 8,
|
||||
},
|
||||
{
|
||||
"city": "Wheaton",
|
||||
"state": "IL",
|
||||
"zip_code": "60189",
|
||||
"latitude": 41.852871,
|
||||
"longitude": -88.109077,
|
||||
"school_type": "Public",
|
||||
"enrollment": 620,
|
||||
"grades": "6-8",
|
||||
"name": "Edison Middle School",
|
||||
"parent_rating": 4,
|
||||
"rating": 4,
|
||||
},
|
||||
{
|
||||
"city": "Wheaton",
|
||||
"state": "IL",
|
||||
"zip_code": "60189",
|
||||
"latitude": 41.851345,
|
||||
"longitude": -88.129822,
|
||||
"school_type": "Public",
|
||||
"enrollment": 507,
|
||||
"grades": "PK-5",
|
||||
"name": "Madison Elementary School",
|
||||
"parent_rating": 4,
|
||||
"rating": 4,
|
||||
},
|
||||
{
|
||||
"city": "Wheaton",
|
||||
"state": "IL",
|
||||
"zip_code": "60189",
|
||||
"latitude": 41.857048,
|
||||
"longitude": -88.108467,
|
||||
"school_type": "Public",
|
||||
"enrollment": 459,
|
||||
"grades": "K-5",
|
||||
"name": "Whittier Elementary School",
|
||||
"parent_rating": 5,
|
||||
"rating": 4,
|
||||
},
|
||||
],
|
||||
"tax_info": {
|
||||
"assessed_value": 202255,
|
||||
"assessment_year": 2024,
|
||||
"tax_amount": 12520.12,
|
||||
"year": 2024,
|
||||
},
|
||||
"sale_info": [
|
||||
{
|
||||
"seq_no": 1,
|
||||
"sale_date": "2025-04-22T00:00:00.000Z",
|
||||
"sale_amount": 0,
|
||||
},
|
||||
{
|
||||
"seq_no": 2,
|
||||
"sale_date": "2020-06-26T00:00:00.000Z",
|
||||
"sale_amount": 475000,
|
||||
},
|
||||
],
|
||||
"owner": 5,
|
||||
"created_at": "2025-08-04",
|
||||
"last_updated": "2025-08-04",
|
||||
"open_houses": [],
|
||||
}
|
||||
|
||||
def test_create_property(self):
|
||||
serializer = PropertySerializer(data=self.property_data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
serializer = PropertyRequestSerializer(data=self.property_data)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
property = serializer.save()
|
||||
self.assertEqual(property.owner, self.owner)
|
||||
self.assertEqual(property.address, '123 Main St')
|
||||
self.assertEqual(property.city, 'Anytown')
|
||||
self.assertEqual(property.state, 'CA')
|
||||
self.assertEqual(property.zip_code, '90210')
|
||||
self.assertEqual(str(property.market_value), '500000.00')
|
||||
self.assertEqual(str(property.loan_amount), '400000.00')
|
||||
self.assertEqual(str(property.loan_interest_rate), '3.50')
|
||||
self.assertEqual(property.address, "123 Main St")
|
||||
self.assertEqual(property.city, "Anytown")
|
||||
self.assertEqual(property.state, "CA")
|
||||
self.assertEqual(property.zip_code, "90210")
|
||||
self.assertEqual(str(property.market_value), "500000.00")
|
||||
self.assertEqual(str(property.loan_amount), "400000.00")
|
||||
self.assertEqual(str(property.loan_interest_rate), "3.50")
|
||||
self.assertEqual(property.loan_term, 30)
|
||||
self.assertEqual(str(property.loan_start_date), '2020-01-01')
|
||||
|
||||
self.assertEqual(str(property.loan_start_date), "2020-01-01")
|
||||
|
||||
def test_create_full_property(self):
|
||||
serializer = PropertyRequestSerializer(data=self.full_property_data)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
property = serializer.save()
|
||||
|
||||
def test_update_property(self):
|
||||
property = Property.objects.create(
|
||||
owner=self.owner,
|
||||
address='123 Main St',
|
||||
city='Anytown',
|
||||
state='CA',
|
||||
zip_code='90210',
|
||||
market_value=500000.00
|
||||
address="123 Main St",
|
||||
city="Anytown",
|
||||
state="CA",
|
||||
zip_code="90210",
|
||||
market_value=500000.00,
|
||||
)
|
||||
|
||||
|
||||
update_data = {
|
||||
'address': '456 New St',
|
||||
'city': 'Newtown',
|
||||
'state': 'NY',
|
||||
'zip_code': '10001',
|
||||
'market_value': '600000.00'
|
||||
"address": "456 New St",
|
||||
"city": "Newtown",
|
||||
"state": "NY",
|
||||
"zip_code": "10001",
|
||||
"market_value": "600000.00",
|
||||
}
|
||||
|
||||
serializer = PropertySerializer(instance=property, data=update_data, partial=True)
|
||||
|
||||
serializer = PropertyRequestSerializer(
|
||||
instance=property, data=update_data, partial=True
|
||||
)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
updated_property = serializer.save()
|
||||
|
||||
self.assertEqual(updated_property.address, '456 New St')
|
||||
self.assertEqual(updated_property.city, 'Newtown')
|
||||
self.assertEqual(updated_property.state, 'NY')
|
||||
self.assertEqual(updated_property.zip_code, '10001')
|
||||
self.assertEqual(str(updated_property.market_value), '600000.00')
|
||||
|
||||
self.assertEqual(updated_property.address, "456 New St")
|
||||
self.assertEqual(updated_property.city, "Newtown")
|
||||
self.assertEqual(updated_property.state, "NY")
|
||||
self.assertEqual(updated_property.zip_code, "10001")
|
||||
self.assertEqual(str(updated_property.market_value), "600000.00")
|
||||
|
||||
|
||||
class VideoSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.category = VideoCategory.objects.create(
|
||||
name='Test Category',
|
||||
description='Test Description'
|
||||
name="Test Category", description="Test Description"
|
||||
)
|
||||
self.video_data = {
|
||||
'category': {
|
||||
'id': self.category.id,
|
||||
'name': self.category.name,
|
||||
'description': self.category.description
|
||||
"category": {
|
||||
"id": self.category.id,
|
||||
"name": self.category.name,
|
||||
"description": self.category.description,
|
||||
},
|
||||
'title': 'Test Video',
|
||||
'description': 'Test Video Description',
|
||||
'link': 'https://example.com/video',
|
||||
'duration': 300
|
||||
"title": "Test Video",
|
||||
"description": "Test Video Description",
|
||||
"link": "https://example.com/video",
|
||||
"duration": 300,
|
||||
}
|
||||
|
||||
|
||||
def test_video_serializer(self):
|
||||
serializer = VideoSerializer(data=self.video_data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
video = serializer.save()
|
||||
self.assertEqual(video.category, self.category)
|
||||
self.assertEqual(video.title, 'Test Video')
|
||||
self.assertEqual(video.description, 'Test Video Description')
|
||||
self.assertEqual(video.link, 'https://example.com/video')
|
||||
self.assertEqual(video.title, "Test Video")
|
||||
self.assertEqual(video.description, "Test Video Description")
|
||||
self.assertEqual(video.link, "https://example.com/video")
|
||||
self.assertEqual(video.duration, 300)
|
||||
|
||||
|
||||
class UserVideoProgressSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='user@example.com',
|
||||
first_name='Test',
|
||||
last_name='User',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="user@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.category = VideoCategory.objects.create(name='Test Category')
|
||||
self.category = VideoCategory.objects.create(name="Test Category")
|
||||
self.video = Video.objects.create(
|
||||
category=self.category,
|
||||
title='Test Video',
|
||||
link='https://example.com/video',
|
||||
duration=300
|
||||
title="Test Video",
|
||||
link="https://example.com/video",
|
||||
duration=300,
|
||||
)
|
||||
self.progress_data = {
|
||||
'video': {
|
||||
'id': self.video.id,
|
||||
'title': self.video.title,
|
||||
'link': self.video.link,
|
||||
'duration': self.video.duration,
|
||||
'category': {
|
||||
'id': self.category.id,
|
||||
'name': self.category.name
|
||||
}
|
||||
"video": {
|
||||
"id": self.video.id,
|
||||
"title": self.video.title,
|
||||
"link": self.video.link,
|
||||
"duration": self.video.duration,
|
||||
"category": {"id": self.category.id, "name": self.category.name},
|
||||
},
|
||||
'user': self.user.pk,
|
||||
'progress': 150
|
||||
"user": self.user.pk,
|
||||
"progress": 150,
|
||||
}
|
||||
|
||||
|
||||
def test_progress_serializer(self):
|
||||
serializer = UserVideoProgressSerializer(data=self.progress_data)
|
||||
|
||||
|
||||
self.assertTrue(serializer.is_valid())
|
||||
progress = serializer.save(user=self.user)
|
||||
self.assertEqual(progress.user, self.user)
|
||||
self.assertEqual(progress.video.title, self.video.title)
|
||||
self.assertEqual(progress.video.category.name, self.video.category.name)
|
||||
self.assertEqual(progress.progress, 150)
|
||||
self.assertEqual(progress.status, 'in_progress')
|
||||
self.assertEqual(progress.status, "in_progress")
|
||||
|
||||
def test_many_progress_serializer(self):
|
||||
categories = [VideoCategory.objects.create(name='Category One'),
|
||||
VideoCategory.objects.create(name='Category Two')]
|
||||
|
||||
categories = [
|
||||
VideoCategory.objects.create(name="Category One"),
|
||||
VideoCategory.objects.create(name="Category Two"),
|
||||
]
|
||||
|
||||
videos = [
|
||||
Video.objects.create(
|
||||
category=categories[0],
|
||||
title='Test Video 1',
|
||||
link='https://example.com/video1',
|
||||
duration=300
|
||||
title="Test Video 1",
|
||||
link="https://example.com/video1",
|
||||
duration=300,
|
||||
),
|
||||
Video.objects.create(
|
||||
category=categories[0],
|
||||
title='Test Video 2',
|
||||
link='https://example.com/video2',
|
||||
duration=300
|
||||
title="Test Video 2",
|
||||
link="https://example.com/video2",
|
||||
duration=300,
|
||||
),
|
||||
Video.objects.create(
|
||||
category=categories[0],
|
||||
title='Test Video 3',
|
||||
link='https://example.com/video3',
|
||||
duration=300
|
||||
title="Test Video 3",
|
||||
link="https://example.com/video3",
|
||||
duration=300,
|
||||
),
|
||||
Video.objects.create(
|
||||
category=categories[1],
|
||||
title='Test Video 4',
|
||||
link='https://example.com/video4',
|
||||
duration=300
|
||||
)
|
||||
title="Test Video 4",
|
||||
link="https://example.com/video4",
|
||||
duration=300,
|
||||
),
|
||||
]
|
||||
|
||||
progress_data = [
|
||||
UserVideoProgress.objects.create(
|
||||
video=videos[0],
|
||||
user=self.user,
|
||||
progress=100
|
||||
video=videos[0], user=self.user, progress=100
|
||||
),
|
||||
UserVideoProgress.objects.create(
|
||||
video=videos[1],
|
||||
user=self.user,
|
||||
progress=30
|
||||
video=videos[1], user=self.user, progress=30
|
||||
),
|
||||
UserVideoProgress.objects.create(
|
||||
video=videos[2],
|
||||
user=self.user,
|
||||
progress=0
|
||||
video=videos[2], user=self.user, progress=0
|
||||
),
|
||||
UserVideoProgress.objects.create(
|
||||
video=videos[3],
|
||||
user=self.user,
|
||||
progress=0
|
||||
video=videos[3], user=self.user, progress=0
|
||||
),
|
||||
]
|
||||
|
||||
serializer = UserVideoProgressSerializer(UserVideoProgress.objects.filter(user=self.user), many=True)
|
||||
serializer = UserVideoProgressSerializer(
|
||||
UserVideoProgress.objects.filter(user=self.user), many=True
|
||||
)
|
||||
self.assertEqual(len(serializer.data), len(progress_data))
|
||||
for i in range(len(serializer.data)):
|
||||
self.assertEqual(serializer.data[i]['video']['title'], progress_data[i].video.title)
|
||||
self.assertEqual(serializer.data[i]['video']['description'], progress_data[i].video.description)
|
||||
self.assertEqual(serializer.data[i]['video']['link'], progress_data[i].video.link)
|
||||
self.assertEqual(serializer.data[i]['video']['duration'], progress_data[i].video.duration)
|
||||
|
||||
self.assertEqual(
|
||||
serializer.data[i]["video"]["title"], progress_data[i].video.title
|
||||
)
|
||||
self.assertEqual(
|
||||
serializer.data[i]["video"]["description"],
|
||||
progress_data[i].video.description,
|
||||
)
|
||||
self.assertEqual(
|
||||
serializer.data[i]["video"]["link"], progress_data[i].video.link
|
||||
)
|
||||
self.assertEqual(
|
||||
serializer.data[i]["video"]["duration"], progress_data[i].video.duration
|
||||
)
|
||||
|
||||
|
||||
class ConversationSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner_user = User.objects.create_user(
|
||||
email='owner@example.com',
|
||||
first_name='Property',
|
||||
last_name='Owner',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(user=self.owner_user)
|
||||
|
||||
|
||||
self.vendor_user = User.objects.create_user(
|
||||
email='vendor@example.com',
|
||||
first_name='Vendor',
|
||||
last_name='User',
|
||||
user_type='vendor',
|
||||
password='testpass123'
|
||||
email="vendor@example.com",
|
||||
first_name="Vendor",
|
||||
last_name="User",
|
||||
user_type="vendor",
|
||||
password="testpass123",
|
||||
)
|
||||
self.vendor = Vendor.objects.create(
|
||||
user=self.vendor_user,
|
||||
business_name='Test Vendor',
|
||||
business_type='contractor'
|
||||
business_name="Test Vendor",
|
||||
business_type="contractor",
|
||||
)
|
||||
|
||||
|
||||
self.property = Property.objects.create(
|
||||
owner=self.owner,
|
||||
address='123 Main St',
|
||||
city='Anytown',
|
||||
state='CA',
|
||||
zip_code='90210',
|
||||
market_value=500000.00
|
||||
address="123 Main St",
|
||||
city="Anytown",
|
||||
state="CA",
|
||||
zip_code="90210",
|
||||
market_value=500000.00,
|
||||
)
|
||||
|
||||
|
||||
self.conversation_data = {
|
||||
'property_owner': self.owner.pk,
|
||||
'vendor': self.vendor.pk,
|
||||
'property': self.property.pk
|
||||
"property_owner": self.owner.pk,
|
||||
"vendor": self.vendor.pk,
|
||||
"property": self.property.pk,
|
||||
}
|
||||
|
||||
|
||||
def test_conversation_serializer(self):
|
||||
serializer = ConversationSerializer(data=self.conversation_data)
|
||||
serializer = ConversationRequestSerializer(data=self.conversation_data)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
conversation = serializer.save()
|
||||
self.assertEqual(conversation.property_owner, self.owner)
|
||||
self.assertEqual(conversation.vendor, self.vendor)
|
||||
self.assertEqual(conversation.property, self.property)
|
||||
|
||||
|
||||
class MessageSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner_user = User.objects.create_user(
|
||||
email='owner@example.com',
|
||||
first_name='Property',
|
||||
last_name='Owner',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(user=self.owner_user)
|
||||
|
||||
|
||||
self.vendor_user = User.objects.create_user(
|
||||
email='vendor@example.com',
|
||||
first_name='Vendor',
|
||||
last_name='User',
|
||||
user_type='vendor',
|
||||
password='testpass123'
|
||||
email="vendor@example.com",
|
||||
first_name="Vendor",
|
||||
last_name="User",
|
||||
user_type="vendor",
|
||||
password="testpass123",
|
||||
)
|
||||
self.vendor = Vendor.objects.create(
|
||||
user=self.vendor_user,
|
||||
business_name='Test Vendor',
|
||||
business_type='contractor'
|
||||
business_name="Test Vendor",
|
||||
business_type="contractor",
|
||||
)
|
||||
|
||||
|
||||
self.property = Property.objects.create(
|
||||
owner=self.owner,
|
||||
address='123 Main St',
|
||||
city='Anytown',
|
||||
state='CA',
|
||||
zip_code='90210',
|
||||
market_value=500000.00
|
||||
address="123 Main St",
|
||||
city="Anytown",
|
||||
state="CA",
|
||||
zip_code="90210",
|
||||
market_value=500000.00,
|
||||
)
|
||||
|
||||
|
||||
self.conversation = Conversation.objects.create(
|
||||
property_owner=self.owner,
|
||||
vendor=self.vendor,
|
||||
property=self.property
|
||||
property_owner=self.owner, vendor=self.vendor, property=self.property
|
||||
)
|
||||
|
||||
self.message_data = {
|
||||
'text': 'Test message'
|
||||
}
|
||||
|
||||
|
||||
self.message_data = {"text": "Test message"}
|
||||
|
||||
def test_message_serializer(self):
|
||||
|
||||
context = {'sender': self.owner_user.pk, 'conversation': self.conversation.pk, 'text': 'Test message'}
|
||||
context = {
|
||||
"sender": self.owner_user.pk,
|
||||
"conversation": self.conversation.pk,
|
||||
"text": "Test message",
|
||||
}
|
||||
serializer = MessageSerializer(data=context)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
|
||||
message = serializer.save()
|
||||
self.assertEqual(message.conversation, self.conversation)
|
||||
self.assertEqual(message.sender, self.owner_user)
|
||||
self.assertEqual(message.text, 'Test message')
|
||||
self.assertEqual(message.text, "Test message")
|
||||
self.assertFalse(message.read)
|
||||
|
||||
|
||||
class PasswordResetRequestSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='user@example.com',
|
||||
first_name='Test',
|
||||
last_name='User',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="user@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.valid_data = {'email': 'user@example.com'}
|
||||
self.invalid_data = {'email': 'nonexistent@example.com'}
|
||||
|
||||
self.valid_data = {"email": "user@example.com"}
|
||||
self.invalid_data = {"email": "nonexistent@example.com"}
|
||||
|
||||
def test_valid_email(self):
|
||||
serializer = PasswordResetRequestSerializer(data=self.valid_data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
|
||||
def test_invalid_email(self):
|
||||
serializer = PasswordResetRequestSerializer(data=self.invalid_data)
|
||||
with self.assertRaises(ValidationError):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
||||
class PasswordResetConfirmSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
email='user@example.com',
|
||||
first_name='Test',
|
||||
last_name='User',
|
||||
user_type='property_owner',
|
||||
password='testpass123'
|
||||
email="user@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.token = uuid.uuid4()
|
||||
self.valid_data = {
|
||||
'token': self.token,
|
||||
'new_password': 'newpass123',
|
||||
'new_password2': 'newpass123'
|
||||
"token": self.token,
|
||||
"new_password": "newpass123",
|
||||
"new_password2": "newpass123",
|
||||
}
|
||||
self.mismatch_data = {
|
||||
'token': self.token,
|
||||
'new_password': 'newpass123',
|
||||
'new_password2': 'differentpass'
|
||||
"token": self.token,
|
||||
"new_password": "newpass123",
|
||||
"new_password2": "differentpass",
|
||||
}
|
||||
|
||||
|
||||
def test_valid_password_reset(self):
|
||||
PasswordResetToken.objects.create(
|
||||
user=self.user,
|
||||
token=self.token,
|
||||
expires_at=datetime.now() + timedelta(hours=24)
|
||||
expires_at=datetime.now() + timedelta(hours=24),
|
||||
)
|
||||
serializer = PasswordResetConfirmSerializer(data=self.valid_data)
|
||||
|
||||
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
|
||||
def test_password_mismatch(self):
|
||||
serializer = PasswordResetConfirmSerializer(data=self.mismatch_data)
|
||||
with self.assertRaises(ValidationError):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
||||
|
||||
class OfferSerializerTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner_user = User.objects.create_user(
|
||||
email="owner@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner = PropertyOwner.objects.create(user=self.owner_user)
|
||||
self.property = Property.objects.create(
|
||||
owner=self.owner,
|
||||
address="78 Midgewood",
|
||||
city="Boardman",
|
||||
state="OH",
|
||||
zip_code="44512",
|
||||
market_value=500000.00,
|
||||
)
|
||||
|
||||
self.owner_user_2 = User.objects.create_user(
|
||||
email="owner2@example.com",
|
||||
first_name="Property",
|
||||
last_name="Owner",
|
||||
user_type="property_owner",
|
||||
password="testpass123",
|
||||
)
|
||||
self.owner_2 = PropertyOwner.objects.create(user=self.owner_user_2)
|
||||
self.property_2 = Property.objects.create(
|
||||
owner=self.owner_2,
|
||||
address="1968 Greensboro Dr",
|
||||
city="wheaton",
|
||||
state="IL",
|
||||
zip_code="60189",
|
||||
market_value=500000.00,
|
||||
)
|
||||
|
||||
self.offer_user = User.objects.create_user(
|
||||
email="vendor@example.com",
|
||||
first_name="Vendor",
|
||||
last_name="User",
|
||||
user_type="vendor",
|
||||
password="testpass123",
|
||||
)
|
||||
|
||||
def test_create_offer_serializer(self):
|
||||
context = {"user": self.offer_user.pk, "property": self.property.pk}
|
||||
serializer = OfferRequestSerializer(data=context)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
offer = serializer.save()
|
||||
self.assertEqual(offer.user, self.offer_user)
|
||||
self.assertEqual(offer.status, "draft")
|
||||
self.assertEqual(offer.property, self.property)
|
||||
|
||||
def test_response_offer_serializer(self):
|
||||
offer = Offer.objects.create(
|
||||
user=self.offer_user,
|
||||
property=self.property,
|
||||
)
|
||||
serializer = OfferResponseSerializer(offer)
|
||||
|
||||
self.assertTrue(serializer.data["status"] == "draft", serializer.data)
|
||||
self.assertTrue(serializer.data["is_active"], serializer.data)
|
||||
self.assertTrue(serializer.data["user"], self.offer_user.pk)
|
||||
self.assertTrue(serializer.data["property"], self.property.pk)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,72 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.9.0
|
||||
asgiref==3.9.0
|
||||
attrs==25.3.0
|
||||
autobahn==24.4.2
|
||||
Automat==25.4.16
|
||||
black==25.1.0
|
||||
certifi==2025.7.14
|
||||
cffi==1.17.1
|
||||
cfgv==3.4.0
|
||||
channels==4.2.2
|
||||
channels_redis==4.2.1
|
||||
charset-normalizer==3.4.2
|
||||
click==8.2.1
|
||||
constantly==23.10.4
|
||||
coverage==7.9.2
|
||||
cryptography==45.0.6
|
||||
daphne==4.2.1
|
||||
distlib==0.3.9
|
||||
Django==5.2.4
|
||||
django-cors-headers==4.7.0
|
||||
django-filter==25.1
|
||||
djangorestframework==3.16.0
|
||||
djangorestframework_simplejwt==5.5.0
|
||||
filelock==3.18.0
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
hyperlink==21.0.0
|
||||
identify==2.6.12
|
||||
idna==3.10
|
||||
incremental==24.7.2
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
langchain-core==0.3.72
|
||||
langchain-ollama==0.3.6
|
||||
langsmith==0.4.9
|
||||
msgpack==1.1.1
|
||||
mypy_extensions==1.1.0
|
||||
nodeenv==1.9.1
|
||||
ollama==0.5.1
|
||||
orjson==3.11.1
|
||||
packaging==25.0
|
||||
pathspec==0.12.1
|
||||
pillow==11.3.0
|
||||
platformdirs==4.3.8
|
||||
pre_commit==4.2.0
|
||||
pyasn1==0.6.1
|
||||
pyasn1_modules==0.4.2
|
||||
pycparser==2.22
|
||||
pydantic==2.11.7
|
||||
pydantic_core==2.33.2
|
||||
PyJWT==2.9.0
|
||||
pyOpenSSL==25.1.0
|
||||
python-decouple==3.8
|
||||
PyYAML==6.0.2
|
||||
redis==6.2.0
|
||||
requests==2.32.4
|
||||
requests-toolbelt==1.0.0
|
||||
service-identity==24.2.0
|
||||
setuptools==80.9.0
|
||||
sniffio==1.3.1
|
||||
sqlparse==0.5.3
|
||||
tenacity==9.1.2
|
||||
Twisted==25.5.0
|
||||
txaio==25.6.1
|
||||
typing-inspection==0.4.1
|
||||
typing_extensions==4.14.1
|
||||
urllib3==2.5.0
|
||||
virtualenv==20.31.2
|
||||
zope.interface==7.2
|
||||
zstandard==0.23.0
|
||||
|
||||
Reference in New Issue
Block a user