diff --git a/.gitignore b/.gitignore index cf4a7d7..6b5517f 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,5 @@ GitHub.sublime-settings !.vscode/extensions.json .history .vercel + +profile_pictures/ diff --git a/assets/invalid_img.jpg b/assets/invalid_img.jpg new file mode 100644 index 0000000..8d60b71 --- /dev/null +++ b/assets/invalid_img.jpg @@ -0,0 +1 @@ +python manage.py dev test authentication.tests diff --git a/assets/logo.jpg b/assets/logo.jpg new file mode 100644 index 0000000..ae3d64e Binary files /dev/null and b/assets/logo.jpg differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..f928fcf Binary files /dev/null and b/assets/logo.png differ diff --git a/authentication/migrations/0002_profile.py b/authentication/migrations/0002_profile.py new file mode 100644 index 0000000..80c3c0e --- /dev/null +++ b/authentication/migrations/0002_profile.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.9 on 2024-02-23 14:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField(blank=True)), + ('profile_picture', models.ImageField(blank=True, upload_to='profile_pictures/')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/authentication/models.py b/authentication/models.py index a7a27e2..878af6c 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -2,4 +2,9 @@ from django.contrib.auth.models import AbstractUser class AppUser(AbstractUser): - is_verified_user = models.BooleanField(default=False) \ No newline at end of file + is_verified_user = models.BooleanField(default=False) + +class Profile(models.Model): + user = models.OneToOneField(AppUser, on_delete=models.CASCADE) + bio = models.TextField(blank=True) + profile_picture = models.ImageField(upload_to='profile_pictures/', blank=True) diff --git a/authentication/serializers.py b/authentication/serializers.py new file mode 100644 index 0000000..8ad527d --- /dev/null +++ b/authentication/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import Profile + +class ProfileSerializer(serializers.ModelSerializer): + class Meta: + model = Profile + fields = ['bio', 'profile_picture'] # Add other fields as needed diff --git a/authentication/tests.py b/authentication/tests.py index bee5a86..89abb56 100644 --- a/authentication/tests.py +++ b/authentication/tests.py @@ -1,10 +1,12 @@ from django.test import TestCase -from authentication.models import AppUser +from authentication.models import AppUser, Profile from django.urls import reverse import json from django.core import mail from rest_framework.test import APIClient from .tokens import account_token +from django.core.files.uploadedfile import SimpleUploadedFile +import os REGISTER_LINK = reverse('authentication:register') LOGIN_LINK = reverse('authentication:login') @@ -68,6 +70,7 @@ def test_missing_fields(self): self.assertEqual(response.status_code, 400) self.assertEqual(response.json()['msg'],"One or more fields are missing!") + class LoginTest(TestCase): def setUp(self): self.client = APIClient() @@ -83,7 +86,7 @@ def test_login_failed(self): response = self.client.post(LOGIN_LINK, json.dumps(data), content_type='application/json') self.assertEqual(response.status_code, 400) self.assertEqual(response.json()['msg'],"Wrong username/password!") - + def test_missing_fields(self): data = { "username":"user1" @@ -113,11 +116,12 @@ def test_valid_verification_token(self): def test_invalid_verification_token(self): response = self.client.post((EMAIL_VERIFICATION_LINK), {'token': 'invalid token'}) self.assertEqual(response.status_code, 400) - + def test_missing_verification_token(self): response = self.client.post((EMAIL_VERIFICATION_LINK), {}) self.assertEqual(response.status_code, 400) + class SendRecoverPasswordEmailTest(TestCase): def setUp(self): self.client = APIClient() @@ -132,11 +136,11 @@ def test_sent_email_recover_password(self): def test_sent_wrong_email_recover_password(self): response = self.client.post((RECOVER_PASSWORD_LINK), {'email':'wrong@email.com'}) self.assertEqual(response.status_code, 400) - + def test_sent_missing_email_recover_password(self): response = self.client.post((RECOVER_PASSWORD_LINK), {}) self.assertEqual(response.status_code, 400) - + def test_change_password(self): token = account_token.make_token(self.user) response = self.client.put((RECOVER_PASSWORD_LINK), @@ -159,3 +163,62 @@ def test_change_password_missing_fields(self): {'email':'email@email.com', 'token': 'wrong token'}) self.assertEqual(response.status_code, 400) +class ProfileUpdateTest(TestCase): + def setUp(self): + self.client = APIClient() + self.user = AppUser.objects.create_user(email='test@example.com', username='testuser', password='test') + self.client.force_authenticate(user=self.user) + self.profile = Profile.objects.create(user=self.user, bio='Old bio') + + def test_update_profile_with_valid_data(self): + # Test updating profile with valid data + data = {'bio': 'New bio'} + response = self.client.put(reverse('authentication:profile'), data) + self.assertEqual(response.status_code, 200) + self.assertEqual(Profile.objects.get(user=self.user).bio, 'New bio') + + def test_update_profile_with_empty_data(self): + # Test updating profile with empty data + data = {'bio': ''} + response = self.client.put(reverse('authentication:profile'), data) + self.assertEqual(response.status_code, 200) + self.assertEqual(Profile.objects.get(user=self.user).bio, '') # Ensure bio is empty + + def test_update_profile_picture(self): + # Test uploading profile picture + # Replace image_path with the path to a valid image file + image_path = 'assets/logo.jpg' + with open(image_path, 'rb') as f: + image = SimpleUploadedFile('image.jpg', f.read(), content_type='image/jpeg') + data = {'profile_picture': image} + response = self.client.put(reverse('authentication:profile'), data) + self.assertEqual(response.status_code, 200) + self.assertTrue(Profile.objects.get(user=self.user).profile_picture) # Ensure profile picture is uploaded + + def test_update_profile_with_invalid_data(self): + # Test updating profile with invalid data (uploading an image as bio) + image_path = 'assets/invalid_img.jpg' # Path to an invalid image file + with open(image_path, 'rb') as f: + image = SimpleUploadedFile(os.path.basename(image_path), f.read(), content_type='image/jpeg') + data = {'bio': 'New bio', 'profile_picture': image} + response = self.client.put(reverse('authentication:profile'), data) + self.assertEqual(response.status_code, 400) # Expect a bad request response + + + def test_update_profile_unauthenticated(self): + # Test updating profile when unauthenticated + self.client.logout() + data = {'bio': 'New bio'} + response = self.client.put(reverse('authentication:profile'), data) + self.assertEqual(response.status_code, 401) # Expect unauthorized response + + def test_update_profile_picture_unauthenticated(self): + # Test uploading profile picture when unauthenticated + self.client.logout() + # Replace image_path with the path to a valid image file + image_path = 'assets/logo.jpg' + with open(image_path, 'rb') as f: + image = SimpleUploadedFile('image.jpg', f.read(), content_type='image/jpeg') + data = {'profile_picture': image} + response = self.client.put(reverse('authentication:profile'), data) + self.assertEqual(response.status_code, 401) # Expect unauthorized response \ No newline at end of file diff --git a/authentication/urls.py b/authentication/urls.py index 9eaa5b4..057ee8d 100644 --- a/authentication/urls.py +++ b/authentication/urls.py @@ -1,5 +1,6 @@ from django.urls import path -from authentication.views import RegisterView, LoginView, SendVerificationEmailView, SendRecoverPasswordEmailView +from authentication.views import RegisterView, LoginView, SendVerificationEmailView, SendRecoverPasswordEmailView, ProfileView +from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenRefreshView app_name = 'authentication' @@ -7,7 +8,9 @@ urlpatterns = [ path('register/', RegisterView.as_view(), name='register'), path('login/', LoginView.as_view(), name='login'), + path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('verify/', SendVerificationEmailView.as_view(), name='verify_email'), path('recover/', SendRecoverPasswordEmailView.as_view(), name='recover_password'), + path('profile/', ProfileView.as_view(), name='profile'), ] \ No newline at end of file diff --git a/authentication/views.py b/authentication/views.py index 3e80f12..aa896cc 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,4 +1,4 @@ -from authentication.models import AppUser +from authentication.models import AppUser, Profile from django.contrib.auth import authenticate from rest_framework.views import APIView from rest_framework.response import Response @@ -10,6 +10,8 @@ from django.template.loader import render_to_string from .tokens import account_token from django.core.mail import EmailMessage +from .serializers import ProfileSerializer +from rest_framework import status class RegisterView(APIView): @@ -125,3 +127,15 @@ def put(self, request): return Response({'msg': 'Invalid verification token!'}, status=400) else: return Response({'msg': "User doesn't exist!"}, status=400) + +class ProfileView(APIView): + permission_classes = [IsAuthenticated] + def put(self, request): + user = request.user + profile = user.profile + serializer = ProfileSerializer(instance=profile, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response({'msg': 'Profile updated successfully!'}) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 263cd8c..1b82594 100644 Binary files a/requirements.txt and b/requirements.txt differ