diff --git a/dta_service/core/migrations/0001_initial.py b/dta_service/core/migrations/0001_initial.py new file mode 100644 index 0000000..c4fd570 --- /dev/null +++ b/dta_service/core/migrations/0001_initial.py @@ -0,0 +1,166 @@ +# Generated by Django 5.2.4 on 2025-07-07 14:51 + +import core.models +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + 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')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.AddField( + 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', + 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')), + ], + ), + migrations.AddField( + 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'), + ), + migrations.AddField( + 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', + 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')), + ], + options={ + 'unique_together': {('user', 'video')}, + }, + ), + migrations.AlterUniqueTogether( + name='conversation', + unique_together={('property_owner', 'vendor', 'property')}, + ), + ] diff --git a/dta_service/core/migrations/0002_alter_conversation_unique_together.py b/dta_service/core/migrations/0002_alter_conversation_unique_together.py new file mode 100644 index 0000000..a7d7328 --- /dev/null +++ b/dta_service/core/migrations/0002_alter_conversation_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.4 on 2025-07-09 19:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='conversation', + unique_together=set(), + ), + ] diff --git a/dta_service/core/models.py b/dta_service/core/models.py index ab0aeb8..3fc7c94 100644 --- a/dta_service/core/models.py +++ b/dta_service/core/models.py @@ -151,8 +151,8 @@ class Conversation(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - class Meta: - unique_together = ('property_owner', 'vendor', 'property') + # class Meta: + # unique_together = ('property_owner', 'vendor', 'property') def __str__(self): return f"Conversation between {self.property_owner} and {self.vendor} about {self.property}" diff --git a/dta_service/core/serializers.py b/dta_service/core/serializers.py index 07cd434..37f01e8 100644 --- a/dta_service/core/serializers.py +++ b/dta_service/core/serializers.py @@ -106,25 +106,44 @@ class VendorSerializer(serializers.ModelSerializer): class Meta: model = Vendor fields = ['user', 'business_name', 'business_type', 'phone_number', - 'address', 'city', 'state', 'zip_code'] + 'address', 'city', 'state', 'zip_code'] + def create(self, validated_data): + # Extract user data user_data = validated_data.pop('user') - user = User.objects.create_user(**user_data) + + # Get or create category + user, _ = User.objects.get_or_create(**user_data) + + # Create video with the category vendor = Vendor.objects.create(user=user, **validated_data) return vendor - + def update(self, instance, validated_data): user_data = validated_data.pop('user', None) + + # Update Vendor 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() - for attr, value in validated_data.items(): - setattr(instance, attr, value) - instance.save() return instance class PropertySerializer(serializers.ModelSerializer): @@ -148,24 +167,73 @@ class VideoSerializer(serializers.ModelSerializer): model = Video fields = ['id', 'category', 'title', 'description', 'link', 'duration', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at'] + + def create(self, validated_data): + # Extract category data + category_data = validated_data.pop('category') + + # Get or create category + category, _ = VideoCategory.objects.get_or_create(**category_data) + + # Create video with the category + video = Video.objects.create(category=category, **validated_data) + return video + + def update(self, instance, validated_data): + # Handle category update if provided + category_data = validated_data.pop('category', None) + if category_data: + category, _ = VideoCategory.objects.get_or_create(**category_data) + instance.category = category + + # Update other fields + for attr, value in validated_data.items(): + setattr(instance, attr, value) + + instance.save() + return instance class UserVideoProgressSerializer(serializers.ModelSerializer): video = VideoSerializer() class Meta: model = UserVideoProgress - fields = ['video', 'progress', 'status', 'last_watched'] + fields = ['video', 'progress', 'status', 'last_watched', 'user'] read_only_fields = ['status', 'last_watched'] + def create(self, validated_data): + # Extract video data + video_data = validated_data.pop('video') + + # Get or create video + video_serializer = VideoSerializer(data=video_data) + video_serializer.is_valid(raise_exception=True) + video = video_serializer.save() + user = validated_data.pop('user') + # Create progress record + progress = UserVideoProgress.objects.create( + user=user, + video=video, + **validated_data + ) + return progress + def update(self, instance, validated_data): + # Handle video update if provided + video_data = validated_data.pop('video', None) + if video_data: + video_serializer = VideoSerializer(instance.video, data=video_data, partial=True) + video_serializer.is_valid(raise_exception=True) + video_serializer.save() + + # Update progress instance.progress = validated_data.get('progress', instance.progress) instance.save() + return instance class ConversationSerializer(serializers.ModelSerializer): - property_owner = PropertyOwnerSerializer() - vendor = VendorSerializer() - property = PropertySerializer() + class Meta: model = Conversation @@ -173,7 +241,7 @@ class ConversationSerializer(serializers.ModelSerializer): read_only_fields = ['id', 'created_at', 'updated_at'] class MessageSerializer(serializers.ModelSerializer): - sender = UserSerializer(read_only=True) + class Meta: model = Message @@ -181,8 +249,34 @@ class MessageSerializer(serializers.ModelSerializer): read_only_fields = ['id', 'timestamp'] def create(self, validated_data): - message = Message.objects.create(**validated_data) + # Extract user data + sender = validated_data.pop('sender') + + message = Message.objects.create(sender=sender, **validated_data) return message + + + def update(self, instance, validated_data): + """ + Handle updates to message fields. + Note: sender and conversation are typically read-only in updates + """ + # Update text if provided + instance.text = validated_data.get('text', instance.text) + + # Update read status if provided + if 'read' in validated_data: + instance.read = validated_data['read'] + + # Handle attachment updates (if needed) + if 'attachment' in validated_data: + # Delete old attachment if exists + if instance.attachment: + instance.attachment.delete() + instance.attachment = validated_data['attachment'] + + instance.save() + return instance class PasswordResetRequestSerializer(serializers.Serializer): email = serializers.EmailField() @@ -210,6 +304,7 @@ class PasswordResetConfirmSerializer(serializers.Serializer): new_password2 = serializers.CharField(write_only=True) def validate(self, attrs): + try: token = PasswordResetToken.objects.get(token=attrs['token']) except PasswordResetToken.DoesNotExist: diff --git a/dta_service/core/templates/password_reset_email.html b/dta_service/core/templates/password_reset_email.html new file mode 100644 index 0000000..a084d3a --- /dev/null +++ b/dta_service/core/templates/password_reset_email.html @@ -0,0 +1,20 @@ + + + + Password Reset + + +

Hello {{ user.first_name }},

+ +

You're receiving this email because you requested a password reset for your account.

+ +

Please click the following link to reset your password:

+ +

{{ reset_url }}

+ +

If you didn't request this, please ignore this email.

+ +

Thanks,
+ The Real Estate App Team

+ + \ No newline at end of file diff --git a/dta_service/core/urls/__init__.py b/dta_service/core/urls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dta_service/core/urls/conversation.py b/dta_service/core/urls/conversation.py new file mode 100644 index 0000000..e7030ee --- /dev/null +++ b/dta_service/core/urls/conversation.py @@ -0,0 +1,21 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter +from core.views import ConversationViewSet, MessageViewSet + +router = DefaultRouter() +router.register(r'', ConversationViewSet, basename='conversation') + +urlpatterns = [ + path('/messages/', MessageViewSet.as_view({ + 'get': 'list', + 'post': 'create' + }), name='conversation-messages'), + path('/messages//', MessageViewSet.as_view({ + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' + }), name='message-detail'), +] + +urlpatterns += router.urls \ No newline at end of file diff --git a/dta_service/core/urls/property.py b/dta_service/core/urls/property.py new file mode 100644 index 0000000..dd3c2f7 --- /dev/null +++ b/dta_service/core/urls/property.py @@ -0,0 +1,8 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter +from core.views import PropertyViewSet + +router = DefaultRouter() +router.register(r'', PropertyViewSet, basename='property') + +urlpatterns = router.urls \ No newline at end of file diff --git a/dta_service/core/urls/property_owner.py b/dta_service/core/urls/property_owner.py new file mode 100644 index 0000000..6610f08 --- /dev/null +++ b/dta_service/core/urls/property_owner.py @@ -0,0 +1,8 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter +from core.views import PropertyOwnerViewSet + +router = DefaultRouter() +router.register(r'', PropertyOwnerViewSet, basename='property-owner') + +urlpatterns = router.urls \ No newline at end of file diff --git a/dta_service/core/urls/vendor.py b/dta_service/core/urls/vendor.py new file mode 100644 index 0000000..61709a2 --- /dev/null +++ b/dta_service/core/urls/vendor.py @@ -0,0 +1,8 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter +from core.views import VendorViewSet + +router = DefaultRouter() +router.register(r'', VendorViewSet, basename='vendor') + +urlpatterns = router.urls \ No newline at end of file diff --git a/dta_service/core/urls/video.py b/dta_service/core/urls/video.py new file mode 100644 index 0000000..7f2385c --- /dev/null +++ b/dta_service/core/urls/video.py @@ -0,0 +1,10 @@ +from django.urls import path +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') + +urlpatterns = router.urls \ No newline at end of file diff --git a/dta_service/core/views.py b/dta_service/core/views.py index aaefd23..c0d20af 100644 --- a/dta_service/core/views.py +++ b/dta_service/core/views.py @@ -32,7 +32,8 @@ class UserRegisterView(generics.CreateAPIView): permission_classes = [permissions.AllowAny] class LogoutView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = (permissions.AllowAny,) + authentication_classes = () def post(self, request): try: @@ -118,10 +119,11 @@ class VideoViewSet(viewsets.ModelViewSet): 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): return UserVideoProgress.objects.filter(user=self.request.user) def perform_create(self, serializer): diff --git a/dta_service/dta_service/settings.py b/dta_service/dta_service/settings.py index feaaa12..d9a24e9 100644 --- a/dta_service/dta_service/settings.py +++ b/dta_service/dta_service/settings.py @@ -11,6 +11,10 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ """ from pathlib import Path +import os + +from datetime import timedelta +from decouple import config # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -25,7 +29,7 @@ SECRET_KEY = 'django-insecure-d!3*0+eki$pqv1n^)_v6&t^o@+-x2-i+8lzf5f%itsnngm3@y7 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] # Application definition @@ -37,6 +41,16 @@ INSTALLED_APPS = [ '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 = [ @@ -58,6 +72,7 @@ TEMPLATES = [ 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ + 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', @@ -67,6 +82,7 @@ TEMPLATES = [ ] WSGI_APPLICATION = 'dta_service.wsgi.application' +ASGI_APPLICATION = 'config.asgi.application' # Database @@ -108,15 +124,89 @@ TIME_ZONE = 'UTC' USE_I18N = True +USE_L10N = True USE_TZ = True +FRONTEND_URL = config("FRONTEND_URL") -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.2/howto/static-files/ -STATIC_URL = 'static/' +# Static files +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + +# Media files +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Custom user model +AUTH_USER_MODEL = 'core.User' + +# REST Framework +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], +} + +# JWT Settings +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, + 'UPDATE_LAST_LOGIN': False, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'JWK_URL': None, + 'LEEWAY': 0, + + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', + + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', + + 'JTI_CLAIM': 'jti', + + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), +} + +# Email settings +# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# EMAIL_HOST = config('SMTP2GO_HOST') +# EMAIL_PORT = config('SMTP2GO_PORT', cast=int) +# EMAIL_USE_TLS = True +# EMAIL_HOST_USER = config('SMTP2GO_USERNAME') +# EMAIL_HOST_PASSWORD = config('SMTP2GO_PASSWORD') +# DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL') + + +# CHANNEL_LAYERS = { +# 'default': { +# 'BACKEND': 'channels_redis.core.RedisChannelLayer', +# 'CONFIG': { +# "hosts": [(config('REDIS_HOST'), config('REDIS_PORT', cast=int))], +# }, +# }, +# } # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +TEST_DISCOVER_PATTERN = "test_*.py" \ No newline at end of file diff --git a/dta_service/dta_service/urls.py b/dta_service/dta_service/urls.py index 6094d68..83068c8 100644 --- a/dta_service/dta_service/urls.py +++ b/dta_service/dta_service/urls.py @@ -21,8 +21,17 @@ from rest_framework_simplejwt.views import ( ) from core.views import ( CustomTokenObtainPairView, LogoutView, - PasswordResetRequestView, PasswordResetConfirmView + PasswordResetRequestView, PasswordResetConfirmView, + UserRegisterView ) +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') urlpatterns = [ path('admin/', admin.site.urls), diff --git a/dta_service/tests/__init__.py b/dta_service/tests/__init__.py new file mode 100644 index 0000000..f1488e3 --- /dev/null +++ b/dta_service/tests/__init__.py @@ -0,0 +1,4 @@ +# core/tests/__init__.py +from .test_models import * +from .test_serializers import * +from .test_views import * \ No newline at end of file diff --git a/dta_service/tests/test_models.py b/dta_service/tests/test_models.py new file mode 100644 index 0000000..9e38437 --- /dev/null +++ b/dta_service/tests/test_models.py @@ -0,0 +1,328 @@ +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 +) +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' + ) + + 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')) + + def test_user_str(self): + 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' + ) + self.owner = PropertyOwner.objects.create( + 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') + + def test_property_owner_str(self): + 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' + ) + 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' + ) + + 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') + + def test_vendor_str(self): + 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' + ) + 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', + market_value=500000.00, + loan_amount=400000.00, + loan_interest_rate=3.5, + loan_term=30, + 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.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') + + def test_property_str(self): + 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' + ) + self.video = Video.objects.create( + category=self.category, + 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.duration, 300) + + def test_video_str(self): + 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' + ) + 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 + ) + self.progress = UserVideoProgress.objects.create( + 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') + + def test_status_update(self): + # Test not started + self.progress.progress = 0 + self.progress.save() + self.assertEqual(self.progress.status, 'not_started') + + # Test completed + self.progress.progress = 300 + self.progress.save() + self.assertEqual(self.progress.status, 'completed') + + # Test in progress + self.progress.progress = 150 + self.progress.save() + self.assertEqual(self.progress.status, 'in_progress') + + def test_progress_str(self): + 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' + ) + 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' + ) + self.vendor = Vendor.objects.create( + user=self.vendor_user, + 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 + ) + + self.conversation = Conversation.objects.create( + 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}" + 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' + ) + 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' + ) + self.vendor = Vendor.objects.create( + user=self.vendor_user, + 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 + ) + + self.conversation = Conversation.objects.create( + 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' + ) + + 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.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' + ) + self.token = PasswordResetToken.objects.create( + 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}") \ No newline at end of file diff --git a/dta_service/tests/test_serializers.py b/dta_service/tests/test_serializers.py new file mode 100644 index 0000000..69df07d --- /dev/null +++ b/dta_service/tests/test_serializers.py @@ -0,0 +1,511 @@ +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 +) +from core.models import ( + PropertyOwner, Vendor, Property, VideoCategory, Video, + UserVideoProgress, Conversation, Message, PasswordResetToken +) +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' + } + + 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') + + def test_password_mismatch(self): + invalid_data = self.valid_data.copy() + 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' + } + 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') + + def test_update_property_owner(self): + user = User.objects.create_user(**self.user_data) + owner = PropertyOwner.objects.create(user=user, phone_number='1234567890') + + update_data = { + 'user': { + 'first_name': 'NewName', + 'last_name': 'NewLast', + 'email': 'newowner@example.com' + }, + 'phone_number': '9876543210' + } + + 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') + +class VendorSerializerTest(TestCase): + def setUp(self): + self.user_data = { + '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' + } + + def test_create_vendor(self): + serializer = VendorSerializer(data=self.vendor_data) + self.assertTrue(serializer.is_valid()) + 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') + + 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' + ) + + update_data = { + 'user': { + 'first_name': 'NewVendor', + 'last_name': 'NewUser', + 'email': 'newvendor@example.com' + }, + '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') + +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' + ) + 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' + } + + def test_create_property(self): + serializer = PropertySerializer(data=self.property_data) + self.assertTrue(serializer.is_valid()) + 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.loan_term, 30) + self.assertEqual(str(property.loan_start_date), '2020-01-01') + + 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 + ) + + update_data = { + 'address': '456 New St', + 'city': 'Newtown', + 'state': 'NY', + 'zip_code': '10001', + 'market_value': '600000.00' + } + + serializer = PropertySerializer(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') + +class VideoSerializerTest(TestCase): + def setUp(self): + self.category = VideoCategory.objects.create( + name='Test Category', + description='Test Description' + ) + self.video_data = { + '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 + } + + 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.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' + ) + 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 + ) + 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 + } + }, + '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') + + def test_many_progress_serializer(self): + 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 + ), + Video.objects.create( + category=categories[0], + 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 + ), + Video.objects.create( + category=categories[1], + title='Test Video 4', + link='https://example.com/video4', + duration=300 + ) + ] + + progress_data = [ + UserVideoProgress.objects.create( + video=videos[0], + user=self.user, + progress=100 + ), + UserVideoProgress.objects.create( + video=videos[1], + user=self.user, + progress=30 + ), + UserVideoProgress.objects.create( + video=videos[2], + user=self.user, + progress=0 + ), + UserVideoProgress.objects.create( + video=videos[3], + user=self.user, + progress=0 + ), + ] + + 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) + + + +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' + ) + 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' + ) + self.vendor = Vendor.objects.create( + user=self.vendor_user, + 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 + ) + + self.conversation_data = { + 'property_owner': self.owner.pk, + 'vendor': self.vendor.pk, + 'property': self.property.pk + } + + def test_conversation_serializer(self): + serializer = ConversationSerializer(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' + ) + 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' + ) + self.vendor = Vendor.objects.create( + user=self.vendor_user, + 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 + ) + + self.conversation = Conversation.objects.create( + property_owner=self.owner, + vendor=self.vendor, + property=self.property + ) + + self.message_data = { + 'text': 'Test message' + } + + def test_message_serializer(self): + + 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.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' + ) + 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' + ) + self.token = uuid.uuid4() + self.valid_data = { + 'token': self.token, + 'new_password': 'newpass123', + 'new_password2': 'newpass123' + } + self.mismatch_data = { + '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) + ) + 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) + \ No newline at end of file diff --git a/dta_service/tests/test_views.py b/dta_service/tests/test_views.py new file mode 100644 index 0000000..4ec3907 --- /dev/null +++ b/dta_service/tests/test_views.py @@ -0,0 +1,588 @@ +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIClient +from rest_framework import status +from django.contrib.auth import get_user_model +from core.models import ( + PropertyOwner, Vendor, Property, VideoCategory, Video, + UserVideoProgress, Conversation, Message, PasswordResetToken +) +from datetime import datetime, timedelta +from django.utils import timezone + +User = get_user_model() + +class AuthenticationTests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + email='test@example.com', + first_name='Test', + last_name='User', + user_type='property_owner', + password='testpass123' + ) + self.login_url = reverse('token_obtain_pair') + self.refresh_url = reverse('token_refresh') + self.logout_url = reverse('logout') + self.register_url = reverse('register') + self.password_reset_url = reverse('password_reset') + self.password_reset_confirm_url = reverse('password_reset_confirm') + + def test_user_login(self): + data = { + 'email': 'test@example.com', + 'password': 'testpass123' + } + response = self.client.post(self.login_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('access', response.data) + self.assertIn('refresh', response.data) + self.assertIn('user', response.data) + + def test_user_login_invalid_credentials(self): + data = { + 'email': 'test@example.com', + 'password': 'wrongpassword' + } + response = self.client.post(self.login_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_token_refresh(self): + # First login to get refresh token + login_data = { + 'email': 'test@example.com', + 'password': 'testpass123' + } + login_response = self.client.post(self.login_url, login_data, format='json') + refresh_token = login_response.data['refresh'] + + # Now refresh the token + refresh_data = { + 'refresh': refresh_token + } + response = self.client.post(self.refresh_url, refresh_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('access', response.data) + + def test_user_logout(self): + # First login to get tokens + login_data = { + 'email': 'test@example.com', + 'password': 'testpass123' + } + login_response = self.client.post(self.login_url, login_data, format='json') + self.assertEqual(login_response.status_code, status.HTTP_200_OK, login_response.text) + + refresh_token = login_response.data['refresh'] + + # Now logout + logout_data = { + 'refresh_token': refresh_token + } + response = self.client.post(self.logout_url, logout_data, format='json') + self.assertEqual(response.status_code, status.HTTP_205_RESET_CONTENT, response.text) + + def test_user_registration(self): + data = { + 'email': 'newuser@example.com', + 'first_name': 'New', + 'last_name': 'User', + 'user_type': 'vendor', + 'password': 'newpass123', + 'password2': 'newpass123' + } + response = self.client.post(self.register_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertTrue(User.objects.filter(email='newuser@example.com').exists()) + + def test_password_reset_request(self): + data = { + 'email': 'test@example.com' + } + response = self.client.post(self.password_reset_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(PasswordResetToken.objects.filter(user=self.user).exists()) + + def test_password_reset_confirm(self): + # First create a reset token + expires_at = timezone.now() + timedelta(hours=24) + token = PasswordResetToken.objects.create( + user=self.user, + expires_at=expires_at + ) + + data = { + 'token': str(token.token), + 'new_password': 'newpass123', + 'new_password2': 'newpass123' + } + response = self.client.post(self.password_reset_confirm_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify password was changed + self.user.refresh_from_db() + self.assertTrue(self.user.check_password('newpass123')) + + # Verify token was marked as used + token.refresh_from_db() + self.assertTrue(token.used) + +class PropertyOwnerViewTests(TestCase): + def setUp(self): + self.client = APIClient() + self.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.user, phone_number='1234567890') + self.other_user = User.objects.create_user( + email='other@example.com', + first_name='Other', + last_name='User', + user_type='property_owner', + password='testpass123' + ) + self.other_owner = PropertyOwner.objects.create(user=self.other_user) + self.url = reverse('property-owner-list') + + # Authenticate + self.client.force_authenticate(user=self.user) + + def test_get_property_owners(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) # Both owners should be visible + + def test_create_property_owner(self): + data = { + 'user': { + 'email': 'newowner@example.com', + 'first_name': 'New', + 'last_name': 'Owner', + 'user_type': 'property_owner', + 'password': 'testpass123' + }, + 'phone_number': '9876543210' + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + self.assertTrue(PropertyOwner.objects.filter(user__email='newowner@example.com').exists()) + + def test_update_property_owner(self): + url = f"{self.url}{self.owner.pk}/" + data = { + 'user': { + 'first_name': 'Updated', + 'last_name': 'Owner', + 'email': 'new_email@email.com', + 'user_type': 'property_owner' + }, + 'phone_number': '9999999999' + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + self.owner.refresh_from_db() + self.assertEqual(self.owner.user.first_name, 'Updated') + self.assertEqual(self.owner.phone_number, '9999999999') + +class VendorViewTests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + 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' + ) + self.url = reverse('vendor-list') + + # Authenticate + self.client.force_authenticate(user=self.user) + + def test_get_vendors(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_create_vendor(self): + data = { + 'user': { + 'email': 'newvendor@example.com', + 'first_name': 'New', + 'last_name': 'Vendor', + 'user_type': 'vendor', + 'password': 'testpass123' + }, + 'business_name': 'New Vendor', + 'business_type': 'inspector', + 'phone_number': '9876543210', + 'address': '456 New St', + 'city': 'Newtown', + 'state': 'NS', + 'zip_code': '54321' + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertTrue(Vendor.objects.filter(business_name='New Vendor').exists()) + + def test_update_vendor(self): + url = f"{self.url}{self.vendor.pk}/" + data = { + 'user': { + 'first_name': 'Updated', + 'last_name': 'Vendor', + }, + 'business_name': 'Updated Vendor', + 'phone_number': '9999999999' + } + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + self.vendor.refresh_from_db() + self.assertEqual(self.vendor.user.first_name, 'Updated') + self.assertEqual(self.vendor.business_name, 'Updated Vendor') + self.assertEqual(self.vendor.phone_number, '9999999999') + +class PropertyViewTests(TestCase): + def setUp(self): + self.client = APIClient() + 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='123 Main St', + city='Anytown', + state='CA', + zip_code='90210', + market_value=500000.00 + ) + self.url = reverse('property-list') + + # Authenticate as property owner + self.client.force_authenticate(user=self.owner_user) + + def test_get_properties(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_create_property(self): + data = { + 'owner': self.owner.pk, + 'address': '456 New St', + 'city': 'Newtown', + 'state': 'NY', + 'zip_code': '10001', + 'market_value': '600000.00' + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED,response.text) + self.assertTrue(Property.objects.filter(address='456 New St').exists()) + + def test_update_property(self): + url = f"{self.url}{self.property.pk}/" + data = { + 'address': '789 Updated St', + 'market_value': '550000.00' + } + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.property.refresh_from_db() + self.assertEqual(self.property.address, '789 Updated St') + self.assertEqual(str(self.property.market_value), '550000.00') + +class VideoViewTests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + email='user@example.com', + first_name='Test', + last_name='User', + user_type='property_owner', + password='testpass123' + ) + self.category = VideoCategory.objects.create( + 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 + ) + self.url = reverse('video-list') + + # Authenticate + self.client.force_authenticate(user=self.user) + + def test_get_videos(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_create_video(self): + data = { + 'category': { + "name": "New Category", + "description": "a description" + }, + 'title': 'New Video', + 'description': 'New Video Description', + 'link': 'https://example.com/new-video', + 'duration': 240 + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + self.assertTrue(Video.objects.filter(title='New Video').exists()) + + def test_update_video(self): + url = f"{self.url}{self.video.pk}/" + data = { + 'title': 'Updated Video', + 'duration': 360 + } + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + self.video.refresh_from_db() + self.assertEqual(self.video.title, 'Updated Video') + self.assertEqual(self.video.duration, 360) + +class UserVideoProgressViewTests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + 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.video = Video.objects.create( + category=self.category, + title='Test Video', + link='https://example.com/video', + duration=300 + ) + self.video_1 = Video.objects.create( + category=self.category, + title='Test Video 1', + link='https://example.com/video_1', + duration=300 + ) + self.video_2 = Video.objects.create( + category=self.category, + title='Test Video 2', + link='https://example.com/video_2', + duration=300 + ) + self.progress = UserVideoProgress.objects.create( + user=self.user, + video=self.video, + progress=150 + ) + UserVideoProgress.objects.create( + user=self.user, + video=self.video_1, + progress=150 + ) + UserVideoProgress.objects.create( + user=self.user, + video=self.video_2, + progress=150 + ) + self.url = reverse('video-progress-list') + + # Authenticate + self.client.force_authenticate(user=self.user) + + def test_get_video_progress(self): + response = self.client.get(f"{self.url}", kwargs={'format','json'}) + breakpoint() + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + breakpoint() + self.assertEqual(len(response.data), 1) + + def test_update_video_progress(self): + url = f"{self.url}{self.progress.pk}/" + data = { + 'progress': 200 + } + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.progress.refresh_from_db() + self.assertEqual(self.progress.progress, 200) + self.assertEqual(self.progress.status, 'in_progress') + +class ConversationViewTests(TestCase): + def setUp(self): + self.client = APIClient() + + # Create owner user + self.owner_user = User.objects.create_user( + email='owner@example.com', + first_name='Property', + last_name='Owner', + user_type='property_owner', + password='testpass123' + ) + self.owner = PropertyOwner.objects.create(user=self.owner_user) + + # Create vendor user + self.vendor_user = User.objects.create_user( + 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' + ) + + # Create property + self.property = Property.objects.create( + owner=self.owner, + address='123 Main St', + city='Anytown', + state='CA', + zip_code='90210', + market_value=500000.00 + ) + + # Create conversation + self.conversation = Conversation.objects.create( + property_owner=self.owner, + vendor=self.vendor, + property=self.property + ) + + self.url = reverse('conversation-list') + + # Authenticate as owner + self.client.force_authenticate(user=self.owner_user) + + def test_get_conversations_as_owner(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_create_conversation(self): + data = { + 'property_owner': self.owner.pk, + 'vendor': self.vendor.pk, + 'property': self.property.pk + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + self.assertTrue(Conversation.objects.filter(property_owner=self.owner, vendor=self.vendor, property=self.property).exists()) + + def test_get_conversations_as_vendor(self): + # Switch to vendor user + self.client.force_authenticate(user=self.vendor_user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + +class MessageViewTests(TestCase): + def setUp(self): + self.client = APIClient() + + # Create owner user + self.owner_user = User.objects.create_user( + email='owner@example.com', + first_name='Property', + last_name='Owner', + user_type='property_owner', + password='testpass123' + ) + self.owner = PropertyOwner.objects.create(user=self.owner_user) + + # Create vendor user + self.vendor_user = User.objects.create_user( + 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' + ) + + # Create property + self.property = Property.objects.create( + owner=self.owner, + address='123 Main St', + city='Anytown', + state='CA', + zip_code='90210', + market_value=500000.00 + ) + + # Create conversation + self.conversation = Conversation.objects.create( + property_owner=self.owner, + vendor=self.vendor, + property=self.property + ) + + # Create message + self.message = Message.objects.create( + conversation=self.conversation, + sender=self.owner_user, + text='Test message' + ) + + self.url = reverse('conversation-messages', kwargs={'conversation_id': self.conversation.pk}) + + # Authenticate as owner + self.client.force_authenticate(user=self.owner_user) + + def test_get_messages(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_create_message(self): + data = { + 'sender': self.owner.pk, + 'text': 'New message', + 'conversation': self.conversation.pk + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + self.assertTrue(Message.objects.filter(text='New message').exists()) + + def test_get_messages_as_vendor(self): + # Switch to vendor user + self.client.force_authenticate(user=self.vendor_user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) \ No newline at end of file