From feffea75236708a572985617f2882541dc3379ad Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Sun, 6 Jul 2025 12:48:59 -0500 Subject: [PATCH] added inital core project --- dta_service/core/__init__.py | 0 dta_service/core/admin.py | 3 + dta_service/core/apps.py | 6 + dta_service/core/migrations/__init__.py | 0 dta_service/core/models.py | 205 +++++++++++++++++++++ dta_service/core/serializers.py | 233 ++++++++++++++++++++++++ dta_service/core/tests.py | 3 + dta_service/core/views.py | 3 + 8 files changed, 453 insertions(+) create mode 100644 dta_service/core/__init__.py create mode 100644 dta_service/core/admin.py create mode 100644 dta_service/core/apps.py create mode 100644 dta_service/core/migrations/__init__.py create mode 100644 dta_service/core/models.py create mode 100644 dta_service/core/serializers.py create mode 100644 dta_service/core/tests.py create mode 100644 dta_service/core/views.py diff --git a/dta_service/core/__init__.py b/dta_service/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dta_service/core/admin.py b/dta_service/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/dta_service/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/dta_service/core/apps.py b/dta_service/core/apps.py new file mode 100644 index 0000000..8115ae6 --- /dev/null +++ b/dta_service/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/dta_service/core/migrations/__init__.py b/dta_service/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dta_service/core/models.py b/dta_service/core/models.py new file mode 100644 index 0000000..ab0aeb8 --- /dev/null +++ b/dta_service/core/models.py @@ -0,0 +1,205 @@ +from django.db import models +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.utils import timezone +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils.html import strip_tags +from django.conf import settings +import uuid +import os + +class UserManager(BaseUserManager): + def create_user(self, email, password=None, **extra_fields): + if not email: + raise ValueError('Users must have an email address') + + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, password=None, **extra_fields): + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + return self.create_user(email, password, **extra_fields) + +class User(AbstractBaseUser, PermissionsMixin): + USER_TYPE_CHOICES = ( + ('property_owner', 'Property Owner'), + ('vendor', 'Vendor'), + ('admin', 'Admin'), + ) + + email = models.EmailField(unique=True) + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + date_joined = models.DateTimeField(default=timezone.now) + + objects = UserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['first_name', 'last_name', 'user_type'] + + def __str__(self): + return self.email + + def get_full_name(self): + return f"{self.first_name} {self.last_name}" + + def get_short_name(self): + return self.first_name + +class PropertyOwner(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) + phone_number = models.CharField(max_length=20, blank=True, null=True) + + def __str__(self): + return self.user.get_full_name() + +class Vendor(models.Model): + BUSINESS_TYPES = ( + ('contractor', 'Contractor'), + ('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) + 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) + + def __str__(self): + return self.business_name + +class Property(models.Model): + owner = models.ForeignKey(PropertyOwner, on_delete=models.CASCADE, related_name='properties') + 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(max_digits=12, decimal_places=2) + loan_amount = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True) + loan_interest_rate = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True) + loan_term = models.IntegerField(blank=True, null=True) + loan_start_date = models.DateField(blank=True, null=True) + + def __str__(self): + return f"{self.address}, {self.city}, {self.state} {self.zip_code}" + +class VideoCategory(models.Model): + name = models.CharField(max_length=100) + description = models.TextField(blank=True, null=True) + + def __str__(self): + return self.name + +class Video(models.Model): + category = models.ForeignKey(VideoCategory, on_delete=models.CASCADE, related_name='videos') + title = models.CharField(max_length=200) + description = models.TextField(blank=True, null=True) + link = models.URLField() + duration = models.IntegerField(help_text="Duration in seconds") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + +class UserVideoProgress(models.Model): + STATUS_CHOICES = ( + ('not_started', 'Not Started'), + ('in_progress', 'In Progress'), + ('completed', 'Completed'), + ) + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='video_progress') + video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='user_progress') + progress = models.IntegerField(default=0, help_text="Progress in seconds") + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='not_started') + last_watched = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ('user', 'video') + + def __str__(self): + return f"{self.user.email} - {self.video.title} - {self.status}" + + def save(self, *args, **kwargs): + # Update status based on progress + if self.progress == 0: + self.status = 'not_started' + elif self.progress >= self.video.duration: + self.status = 'completed' + self.progress = self.video.duration + else: + self.status = 'in_progress' + super().save(*args, **kwargs) + +class Conversation(models.Model): + property_owner = models.ForeignKey(PropertyOwner, on_delete=models.CASCADE, related_name='conversations') + vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='conversations') + property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name='conversations') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ('property_owner', 'vendor', 'property') + + def __str__(self): + return f"Conversation between {self.property_owner} and {self.vendor} about {self.property}" + +def message_file_path(instance, filename): + ext = filename.split('.')[-1] + filename = f"{uuid.uuid4()}.{ext}" + return os.path.join('message_attachments', filename) + +class Message(models.Model): + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages') + sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages') + text = models.TextField() + attachment = models.FileField(upload_to=message_file_path, blank=True, null=True) + timestamp = models.DateTimeField(auto_now_add=True) + read = models.BooleanField(default=False) + + def __str__(self): + return f"Message from {self.sender} in {self.conversation}" + +class PasswordResetToken(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + used = models.BooleanField(default=False) + + def is_valid(self): + return not self.used and self.expires_at > timezone.now() + + def send_reset_email(self): + subject = "Password Reset Request" + reset_url = f"{settings.FRONTEND_URL}/reset-password/{self.token}/" + context = { + 'user': self.user, + 'reset_url': reset_url, + } + html_message = render_to_string('password_reset_email.html', context) + plain_message = strip_tags(html_message) + send_mail( + subject, + plain_message, + settings.DEFAULT_FROM_EMAIL, + [self.user.email], + html_message=html_message, + fail_silently=False, + ) + + def __str__(self): + return f"Password reset token for {self.user.email}" \ No newline at end of file diff --git a/dta_service/core/serializers.py b/dta_service/core/serializers.py new file mode 100644 index 0000000..07cd434 --- /dev/null +++ b/dta_service/core/serializers.py @@ -0,0 +1,233 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from rest_framework_simplejwt.tokens import RefreshToken +from .models import ( + PropertyOwner, Vendor, Property, VideoCategory, Video, + UserVideoProgress, Conversation, Message, PasswordResetToken +) +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils.html import strip_tags +from django.conf import settings +import uuid +from datetime import datetime, timedelta + +User = get_user_model() + +class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + + # Add custom claims + token['email'] = user.email + token['first_name'] = user.first_name + token['last_name'] = user.last_name + token['user_type'] = user.user_type + + return token + + def validate(self, attrs): + data = super().validate(attrs) + + # Add additional responses + refresh = self.get_token(self.user) + data['refresh'] = str(refresh) + data['access'] = str(refresh.access_token) + + # Add user details + data['user'] = { + 'email': self.user.email, + 'first_name': self.user.first_name, + 'last_name': self.user.last_name, + 'user_type': self.user.user_type, + } + + return data + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'email', 'first_name', 'last_name', 'user_type', 'is_active', 'date_joined'] + read_only_fields = ['id', 'is_active', 'date_joined'] + +class UserRegisterSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, required=True) + password2 = serializers.CharField(write_only=True, required=True) + + class Meta: + model = User + fields = ['email', 'first_name', 'last_name', 'user_type', 'password', 'password2'] + extra_kwargs = { + 'password': {'write_only': True}, + 'password2': {'write_only': True}, + } + + def validate(self, attrs): + if attrs['password'] != attrs['password2']: + raise serializers.ValidationError({"password": "Password fields didn't match."}) + return attrs + + def create(self, validated_data): + validated_data.pop('password2') + user = User.objects.create_user(**validated_data) + return user + +class PropertyOwnerSerializer(serializers.ModelSerializer): + user = UserSerializer() + + class Meta: + model = PropertyOwner + fields = ['user', 'phone_number'] + + def create(self, validated_data): + user_data = validated_data.pop('user') + user = User.objects.create_user(**user_data) + property_owner = PropertyOwner.objects.create(user=user, **validated_data) + return property_owner + + def update(self, instance, validated_data): + user_data = validated_data.pop('user', None) + if user_data: + user = instance.user + for attr, value in user_data.items(): + setattr(user, attr, value) + user.save() + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + +class VendorSerializer(serializers.ModelSerializer): + user = UserSerializer() + + class Meta: + model = Vendor + fields = ['user', 'business_name', 'business_type', 'phone_number', + 'address', 'city', 'state', 'zip_code'] + + def create(self, validated_data): + user_data = validated_data.pop('user') + user = User.objects.create_user(**user_data) + vendor = Vendor.objects.create(user=user, **validated_data) + return vendor + + def update(self, instance, validated_data): + user_data = validated_data.pop('user', None) + if user_data: + user = instance.user + for attr, value in user_data.items(): + setattr(user, attr, value) + user.save() + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + +class PropertySerializer(serializers.ModelSerializer): + 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'] + +class VideoCategorySerializer(serializers.ModelSerializer): + class Meta: + model = VideoCategory + fields = ['id', 'name', 'description'] + read_only_fields = ['id'] + +class VideoSerializer(serializers.ModelSerializer): + category = VideoCategorySerializer() + + class Meta: + model = Video + fields = ['id', 'category', 'title', 'description', 'link', 'duration', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at'] + +class UserVideoProgressSerializer(serializers.ModelSerializer): + video = VideoSerializer() + + class Meta: + model = UserVideoProgress + fields = ['video', 'progress', 'status', 'last_watched'] + read_only_fields = ['status', 'last_watched'] + + def update(self, instance, validated_data): + instance.progress = validated_data.get('progress', instance.progress) + instance.save() + return instance + +class ConversationSerializer(serializers.ModelSerializer): + property_owner = PropertyOwnerSerializer() + vendor = VendorSerializer() + property = PropertySerializer() + + 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): + sender = UserSerializer(read_only=True) + + class Meta: + model = Message + fields = ['id', 'conversation', 'sender', 'text', 'attachment', 'timestamp', 'read'] + read_only_fields = ['id', 'timestamp'] + + def create(self, validated_data): + message = Message.objects.create(**validated_data) + return message + +class PasswordResetRequestSerializer(serializers.Serializer): + email = serializers.EmailField() + + def validate_email(self, value): + try: + User.objects.get(email=value) + except User.DoesNotExist: + raise serializers.ValidationError("No user with this email address exists.") + return value + + def save(self): + user = User.objects.get(email=self.validated_data['email']) + expires_at = datetime.now() + timedelta(hours=24) + token = PasswordResetToken.objects.create( + user=user, + expires_at=expires_at + ) + token.send_reset_email() + return token + +class PasswordResetConfirmSerializer(serializers.Serializer): + token = serializers.UUIDField() + new_password = serializers.CharField(write_only=True) + new_password2 = serializers.CharField(write_only=True) + + def validate(self, attrs): + try: + token = PasswordResetToken.objects.get(token=attrs['token']) + except PasswordResetToken.DoesNotExist: + raise serializers.ValidationError({"token": "Invalid token."}) + + if not token.is_valid(): + raise serializers.ValidationError({"token": "Token is invalid or has expired."}) + + if attrs['new_password'] != attrs['new_password2']: + raise serializers.ValidationError({"new_password": "Password fields didn't match."}) + + return attrs + + def save(self): + token = PasswordResetToken.objects.get(token=self.validated_data['token']) + user = token.user + user.set_password(self.validated_data['new_password']) + user.save() + token.used = True + token.save() + return user \ No newline at end of file diff --git a/dta_service/core/tests.py b/dta_service/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dta_service/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dta_service/core/views.py b/dta_service/core/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/dta_service/core/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.