Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profile Fields #570

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions backend/api/migrations/0013_profile_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-12-15 23:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0012_contentsuggestion_type'),
]

operations = [
migrations.AddField(
model_name='profile',
name='description',
field=models.TextField(blank=True, default='', help_text='A brief description about the user or their profile.', max_length=500),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-12-15 23:09

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0013_profile_description'),
]

operations = [
migrations.AddField(
model_name='profile',
name='date_of_birth',
field=models.DateField(blank=True, help_text="User's date of birth (used to calculate age).", null=True),
),
migrations.AddField(
model_name='profile',
name='profile_picture',
field=models.ImageField(blank=True, help_text='Upload a profile picture.', null=True, upload_to='profile_pictures/'),
),
]
32 changes: 30 additions & 2 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,26 @@ class Profile(models.Model):
choices=UserLabel.choices,
blank=False
)

# Description field
description = models.TextField(
max_length=500,
blank=True,
default='',
help_text="A brief description about the user or their profile."
)
# Profile Picture
profile_picture = models.ImageField(
upload_to='profile_pictures/',
blank=True,
null=True,
help_text="Upload a profile picture."
)
# Age (derived from date of birth)
date_of_birth = models.DateField(
blank=True,
null=True,
help_text="User's date of birth (used to calculate age)."
)
# Following and Followers relationships
following = models.ManyToManyField(
'self',
Expand All @@ -44,6 +63,15 @@ def get_following(self):
def get_followers(self):
return self.followers.all()

def calculate_age(self):
"""Calculate the user's age based on their date of birth."""
if self.date_of_birth:
today = date.today()
return today.year - self.date_of_birth.year - (
(today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)
)
return None

class PasswordReset(models.Model):
email = models.EmailField()
token = models.CharField(max_length=100)
Expand Down Expand Up @@ -156,6 +184,6 @@ class SpotifyToken(models.Model):
access_token = models.TextField()
refresh_token = models.TextField()
expires_at = models.DateTimeField() # Add this field

def __str__(self):
return f"{self.user.username}'s Spotify Token"
106 changes: 106 additions & 0 deletions backend/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,109 @@ def test_search_case_insensitive(self):
data = response.json()
self.assertEqual(data['total_results'], 1)
self.assertEqual(data['contents'][0]['description'], 'First content description')
class EditProfileTests(TestCase):
def setUp(self):
"""Set up a test user and profile."""
self.client = Client()
self.user = User.objects.create_user(username='testuser', password='testpass')
self.profile = Profile.objects.create(
user=self.user,
name='OriginalName',
surname='OriginalSurname',
description='Original description'
)
self.url = reverse('edit_profile')

def test_edit_profile_success(self):
"""Test successful profile update."""
self.client.login(username='testuser', password='testpass')
data = {
'name': 'UpdatedName',
'surname': 'UpdatedSurname',
'description': 'Updated description'
}
response = self.client.post(self.url, data=data, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.name, 'UpdatedName')
self.assertEqual(self.profile.surname, 'UpdatedSurname')
self.assertEqual(self.profile.description, 'Updated description')

def test_partial_update(self):
"""Test partial update with only one field."""
self.client.login(username='testuser', password='testpass')
data = {'name': 'PartialUpdate'}
response = self.client.post(self.url, data=data, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.name, 'PartialUpdate')
self.assertEqual(self.profile.surname, 'OriginalSurname')
self.assertEqual(self.profile.description, 'Original description')

def test_invalid_request_format(self):
"""Test invalid request format."""
self.client.login(username='testuser', password='testpass')
response = self.client.post(self.url, data="Invalid Data", content_type='application/json')
self.assertEqual(response.status_code, 400)

def test_unauthenticated_request(self):
"""Test unauthenticated user trying to edit profile."""
data = {'name': 'ShouldFail'}
response = self.client.post(self.url, data=data, content_type='application/json')
self.assertEqual(response.status_code, 302) # Should redirect to login
from django.core.files.uploadedfile import SimpleUploadedFile

class AddProfilePictureTests(TestCase):
def setUp(self):
"""Set up a test user and profile."""
self.client = Client()
self.user = User.objects.create_user(username='testuser', password='testpass')
self.profile = Profile.objects.create(
user=self.user,
name='OriginalName',
surname='OriginalSurname',
description='Original description'
)
self.url = reverse('add_profile_picture')

def test_add_profile_picture_success(self):
"""Test successfully uploading a profile picture."""
self.client.login(username='testuser', password='testpass')
picture = SimpleUploadedFile("test.jpg", b"file_content", content_type="image/jpeg")
response = self.client.post(self.url, {'profile_picture': picture})
self.assertEqual(response.status_code, 200)
self.profile.refresh_from_db()
self.assertIsNotNone(self.profile.profile_picture)
self.assertIn('test.jpg', self.profile.profile_picture.name)

def test_replace_profile_picture(self):
"""Test replacing an existing profile picture."""
self.client.login(username='testuser', password='testpass')
# Add an initial profile picture
initial_picture = SimpleUploadedFile("initial.jpg", b"file_content", content_type="image/jpeg")
self.client.post(self.url, {'profile_picture': initial_picture})
self.profile.refresh_from_db()
initial_picture_path = self.profile.profile_picture.path

# Add a new profile picture
new_picture = SimpleUploadedFile("new.jpg", b"new_file_content", content_type="image/jpeg")
response = self.client.post(self.url, {'profile_picture': new_picture})
self.assertEqual(response.status_code, 200)
self.profile.refresh_from_db()
self.assertIn('new.jpg', self.profile.profile_picture.name)

# Check that the initial picture has been deleted
from os.path import exists
self.assertFalse(exists(initial_picture_path))

def test_missing_profile_picture(self):
"""Test trying to upload without providing a file."""
self.client.login(username='testuser', password='testpass')
response = self.client.post(self.url, {})
self.assertEqual(response.status_code, 400)

def test_unauthenticated_request(self):
"""Test unauthenticated user trying to upload a profile picture."""
picture = SimpleUploadedFile("test.jpg", b"file_content", content_type="image/jpeg")
response = self.client.post(self.url, {'profile_picture': picture})
self.assertEqual(response.status_code, 302) # Should redirect to login
79 changes: 77 additions & 2 deletions backend/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from django.shortcuts import redirect
from urllib.parse import quote
from django.http import JsonResponse, HttpResponseRedirect
from django.core.files.storage import default_storage



@require_http_methods(["POST"])
Expand Down Expand Up @@ -132,6 +134,7 @@ def register(request):
email = data['email']
password = data['password']
labels = data['labels'] # Expecting labels like ['Artist', 'Listener']
age = data.get('age', None)

if User.objects.filter(username=username).exists():
return JsonResponse({'error': 'Username already exists'}, status=400)
Expand Down Expand Up @@ -1948,6 +1951,79 @@ def add_track_to_playlist(request, playlist_id):
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)

@login_required
@require_http_methods(["POST", "PUT"])
def edit_profile(request):
"""
Edit the logged-in user's profile.
The request body should contain 'name', 'surname', 'labels', and 'description'.
"""
try:
user = request.user
profile = Profile.objects.get(user=user)

# Parse JSON data from the request body
data = json.loads(request.body)

# Update fields if provided
profile.name = data.get('name', profile.name)
profile.surname = data.get('surname', profile.surname)
profile.description = data.get('description', profile.description)
profile.date_of_birth = data.get('date_of_birth', profile.date_of_birth)
# Save changes
profile.save()

# Return updated profile details
return JsonResponse({
"success": True,
"message": "Profile updated successfully",
"profile": {
"name": profile.name,
"surname": profile.surname,
"description": profile.description,
"date_of_birth": profile.date_of_birth
}
})

except Profile.DoesNotExist:
return JsonResponse({"success": False, "error": "Profile does not exist"}, status=404)
except json.JSONDecodeError:
return JsonResponse({"success": False, "error": "Invalid JSON data"}, status=400)
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=500)
@login_required
@require_http_methods(["POST"])
def add_profile_picture(request):
"""
Add or update the profile picture for the logged-in user.
"""
try:
user = request.user
profile = Profile.objects.get(user=user)

# Check if a file is uploaded
if 'profile_picture' not in request.FILES:
return JsonResponse({"success": False, "error": "No profile picture provided"}, status=400)

# Delete the old picture if it exists
if profile.profile_picture:
default_storage.delete(profile.profile_picture.path)

# Save the new profile picture
profile.profile_picture = request.FILES['profile_picture']
profile.save()

return JsonResponse({
"success": True,
"message": "Profile picture updated successfully",
"profile_picture_url": profile.profile_picture.url
})

except Profile.DoesNotExist:
return JsonResponse({"success": False, "error": "Profile does not exist"}, status=404)
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=500)

def get_user_spotify_tracks(request, user_id):
"""Get liked songs for a specific user."""
try:
Expand Down Expand Up @@ -2043,5 +2119,4 @@ def get_user_spotify_tracks(request, user_id):
"error": f"Failed to communicate with Spotify API: {str(e)}"
}, status=503)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)

return JsonResponse({"error": str(e)}, status=500)
2 changes: 2 additions & 0 deletions backend/music_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
path('api/spotify/playlist/<str:playlist_id>/', views.get_playlist_details, name='get_playlist_details'),
path('api/spotify/playlist/<str:playlist_id>/tracks/', views.add_track_to_playlist, name='add_track_to_playlist'),
path('api/spotify/get_user_spotify_playlists/<int:user_id>/', views.get_user_spotify_playlists, name='get_user_spotify_playlists'),
path('api/edit_profile/', views.edit_profile, name='edit_profile'),
path('api/add_profile_picture/', views.add_profile_picture, name='add_profile_picture'),
path('api/spotify/get_user_spotify_tracks/<int:user_id>/', views.get_user_spotify_tracks, name='get_user_spotify_tracks'),
# New endpoint
]
49 changes: 45 additions & 4 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,56 @@
aiohappyeyeballs==2.4.4
aiohttp==3.11.10
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.7.0
asgiref==3.8.1
attrs==24.2.0
beautifulsoup4==4.12.3
bs4==0.0.2
certifi==2024.2.2
charset-normalizer==3.3.2
dataclasses-json==0.6.7
distro==1.9.0
Django==5.0.4
django-cors-headers==4.3.1
frozenlist==1.5.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
httpx-sse==0.4.0
idna==3.7
jiter==0.8.2
jsonpatch==1.33
jsonpointer==3.0.0
langchain==0.3.12
langchain-community==0.3.12
langchain-core==0.3.25
langchain-text-splitters==0.3.3
langsmith==0.2.3
marshmallow==3.23.1
multidict==6.1.0
mypy-extensions==1.0.0
numpy==2.2.0
openai==1.57.4
orjson==3.10.12
packaging==24.2
pillow==11.0.0
propcache==0.2.1
psycopg==3.1.18
pydantic==2.10.3
pydantic-settings==2.7.0
pydantic_core==2.27.1
python-dotenv==1.0.1
PyYAML==6.0.2
requests==2.31.0
requests-toolbelt==1.0.0
sniffio==1.3.1
soupsieve==2.6
SQLAlchemy==2.0.36
sqlparse==0.5.0
typing_extensions==4.11.0
tenacity==9.0.0
tqdm==4.67.1
typing-inspect==0.9.0
typing_extensions==4.12.2
urllib3==2.2.1
langchain-community>=<latest-version>
openai>=<latest-version>
bs4>=<latest-version>
yarl==1.18.3