Skip to content

Commit 13e506e

Browse files
committed
changed watchlist element + fixed django workflow
1 parent 817bca9 commit 13e506e

24 files changed

+2022
-266
lines changed

.github/workflows/django.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
max-parallel: 4
1515
matrix:
16-
python-version: [3.7, 3.8, 3.9]
16+
python-version: [3.9, "3.10", "3.11"]
1717

1818
steps:
1919
- uses: actions/checkout@v4
@@ -24,7 +24,9 @@ jobs:
2424
- name: Install Dependencies
2525
run: |
2626
python -m pip install --upgrade pip
27+
cd backend
2728
pip install -r requirements.txt
2829
- name: Run Tests
2930
run: |
31+
cd backend
3032
python manage.py test
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Django 4.2.20 on 2025-05-24 14:38
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('api', '0004_update_existing_avatars'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='WatchlistItem',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('imdb_id', models.CharField(max_length=20)),
21+
('title', models.CharField(max_length=200)),
22+
('year', models.CharField(max_length=4)),
23+
('poster', models.URLField(max_length=500)),
24+
('added_at', models.DateTimeField(auto_now_add=True)),
25+
('watched', models.BooleanField(default=False)),
26+
('rating', models.IntegerField(blank=True, null=True)),
27+
('notes', models.TextField(blank=True)),
28+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='watchlist', to=settings.AUTH_USER_MODEL)),
29+
],
30+
options={
31+
'ordering': ['-added_at'],
32+
'unique_together': {('user', 'imdb_id')},
33+
},
34+
),
35+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.20 on 2025-05-24 15:28
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0005_watchlistitem'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='watchlistitem',
15+
name='imdb_rating',
16+
field=models.CharField(blank=True, max_length=4, null=True),
17+
),
18+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.20 on 2025-05-24 15:45
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0006_watchlistitem_imdb_rating'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='userprofile',
15+
name='avatar',
16+
field=models.URLField(default='https://api.dicebear.com/7.x/initials/svg'),
17+
),
18+
]

backend/api/models.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,37 @@
55
# Create your models here.
66
class UserProfile(models.Model):
77
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
8-
avatar = models.URLField(default='https://api.dicebear.com/7.x/bottts/svg') # Default avatar URL
8+
avatar = models.URLField(default='https://api.dicebear.com/7.x/initials/svg') # Default avatar URL
99
bio = models.TextField(max_length=500, blank=True)
1010

1111
def __str__(self):
1212
return f"{self.user.username}'s profile"
1313

1414
def save(self, *args, **kwargs):
1515
if not self.avatar:
16-
# Generate unique avatar URL using username
17-
self.avatar = f'https://api.dicebear.com/7.x/bottts/svg?seed={self.user.username}'
16+
# Generate unique avatar URL using username and initials style
17+
self.avatar = f'https://api.dicebear.com/7.x/initials/svg?seed={self.user.username}&backgroundColor=9370DB'
1818
super().save(*args, **kwargs)
1919

20+
class WatchlistItem(models.Model):
21+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='watchlist')
22+
imdb_id = models.CharField(max_length=20)
23+
title = models.CharField(max_length=200)
24+
year = models.CharField(max_length=4)
25+
poster = models.URLField(max_length=500)
26+
added_at = models.DateTimeField(auto_now_add=True)
27+
watched = models.BooleanField(default=False)
28+
rating = models.IntegerField(null=True, blank=True)
29+
notes = models.TextField(blank=True)
30+
imdb_rating = models.CharField(max_length=4, blank=True, null=True)
31+
32+
class Meta:
33+
ordering = ['-added_at']
34+
unique_together = ['user', 'imdb_id']
35+
36+
def __str__(self):
37+
return f"{self.title} ({self.year}) - {self.user.username}'s list"
38+
2039
class Note(models.Model):
2140
title = models.CharField(max_length=100)
2241
content = models.TextField()

backend/api/serializers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.contrib.auth.models import User
22
from rest_framework import serializers
3-
from .models import Note, UserProfile
3+
from .models import Note, UserProfile, WatchlistItem
44

55
class UserProfileSerializer(serializers.ModelSerializer):
66
class Meta:
@@ -50,7 +50,7 @@ def get_author_avatar(self, obj):
5050
try:
5151
return obj.author.profile.avatar
5252
except UserProfile.DoesNotExist:
53-
return f'https://api.dicebear.com/7.x/bottts/svg?seed={obj.author.username}'
53+
return f'https://api.dicebear.com/7.x/initials/svg?seed={obj.author.username}&backgroundColor=9370DB'
5454

5555
class NoteReplySerializer(serializers.ModelSerializer):
5656
author_username = serializers.CharField(source='author.username', read_only=True)
@@ -66,4 +66,10 @@ def get_author_avatar(self, obj):
6666
try:
6767
return obj.author.profile.avatar
6868
except UserProfile.DoesNotExist:
69-
return f'https://api.dicebear.com/7.x/bottts/svg?seed={obj.author.username}'
69+
return f'https://api.dicebear.com/7.x/initials/svg?seed={obj.author.username}&backgroundColor=9370DB'
70+
71+
class WatchlistItemSerializer(serializers.ModelSerializer):
72+
class Meta:
73+
model = WatchlistItem
74+
fields = ['id', 'imdb_id', 'title', 'year', 'poster', 'added_at', 'watched', 'rating', 'notes', 'imdb_rating']
75+
read_only_fields = ['added_at']

backend/api/tests.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,118 @@
11
from django.test import TestCase
2+
from django.contrib.auth.models import User
3+
from django.urls import reverse
4+
from rest_framework import status
5+
from rest_framework.test import APIClient
6+
from .models import UserProfile, WatchlistItem, Note
7+
from .serializers import UserSerializer, WatchlistItemSerializer, NoteSerializer
28

3-
# Create your tests here.
9+
class APITestCase(TestCase):
10+
def setUp(self):
11+
self.client = APIClient()
12+
self.user = User.objects.create_user(
13+
username='testuser',
14+
email='test@example.com',
15+
password='testpass123'
16+
)
17+
self.client.force_authenticate(user=self.user)
18+
19+
self.watchlist_item = WatchlistItem.objects.create(
20+
user=self.user,
21+
imdb_id='tt0111161',
22+
title='The Shawshank Redemption',
23+
year='1994',
24+
poster='https://example.com/poster.jpg',
25+
imdb_rating='9.3'
26+
)
27+
28+
self.note = Note.objects.create(
29+
title='Test Note',
30+
content='Test Content',
31+
author=self.user
32+
)
33+
34+
def test_user_profile(self):
35+
"""Test user profile endpoint"""
36+
response = self.client.get(reverse('user-profile'))
37+
self.assertEqual(response.status_code, status.HTTP_200_OK)
38+
self.assertEqual(response.data['username'], 'testuser')
39+
self.assertEqual(response.data['email'], 'test@example.com')
40+
41+
def test_watchlist_crud(self):
42+
"""Test watchlist CRUD operations"""
43+
response = self.client.get(reverse('watchlist'))
44+
self.assertEqual(response.status_code, status.HTTP_200_OK)
45+
self.assertEqual(len(response.data), 1)
46+
47+
new_item = {
48+
'imdb_id': 'tt0068646',
49+
'title': 'The Godfather',
50+
'year': '1972',
51+
'poster': 'https://example.com/godfather.jpg',
52+
'imdb_rating': '9.2'
53+
}
54+
response = self.client.post(reverse('watchlist'), new_item, format='json')
55+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
56+
57+
update_data = {'watched': True, 'rating': 5}
58+
response = self.client.put(
59+
reverse('watchlist-detail', args=[self.watchlist_item.id]),
60+
update_data,
61+
format='json'
62+
)
63+
self.assertEqual(response.status_code, status.HTTP_200_OK)
64+
self.assertEqual(response.data['watched'], True)
65+
self.assertEqual(response.data['rating'], 5)
66+
67+
response = self.client.delete(reverse('watchlist-detail', args=[self.watchlist_item.id]))
68+
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
69+
70+
def test_notes_crud(self):
71+
"""Test notes CRUD operations"""
72+
response = self.client.get(reverse('note-list'))
73+
self.assertEqual(response.status_code, status.HTTP_200_OK)
74+
self.assertEqual(len(response.data), 1)
75+
76+
new_note = {
77+
'title': 'New Note',
78+
'content': 'New Content',
79+
'author': self.user.id
80+
}
81+
response = self.client.post(reverse('note-list'), new_note, format='json')
82+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
83+
84+
update_data = {
85+
'title': 'Updated Note',
86+
'content': 'Updated Content',
87+
'author': self.user.id
88+
}
89+
response = self.client.put(
90+
reverse('note-detail', args=[self.note.id]),
91+
update_data,
92+
format='json'
93+
)
94+
self.assertEqual(response.status_code, status.HTTP_200_OK)
95+
self.assertEqual(response.data['content'], 'Updated Content')
96+
self.assertEqual(response.data['title'], 'Updated Note')
97+
98+
response = self.client.delete(reverse('note-detail', args=[self.note.id]))
99+
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
100+
101+
def test_search_movies(self):
102+
"""Test movie search endpoint"""
103+
response = self.client.get(reverse('search-movies'), {'q': 'Inception'})
104+
self.assertEqual(response.status_code, status.HTTP_200_OK)
105+
self.assertIn('Search', response.data)
106+
107+
def test_unauthorized_access(self):
108+
"""Test unauthorized access to protected endpoints"""
109+
unauthorized_client = APIClient()
110+
111+
response = unauthorized_client.get(reverse('watchlist'))
112+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
113+
114+
response = unauthorized_client.get(reverse('note-list'))
115+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
116+
117+
response = unauthorized_client.get(reverse('user-profile'))
118+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

backend/api/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
path("notes/delete/<int:pk>/", views.NoteDelete.as_view(), name="note-delete"),
88
path("users/create/", views.CreateUserView.as_view(), name="create-user"),
99
path("profile/", views.UserProfileView.as_view(), name="user-profile"),
10-
path("search-movie/", views.SearchOMDbView.as_view(), name="search-movies"),
10+
path("search/", views.SearchOMDbView.as_view(), name="search-movies"),
1111
path("spotify/token/", views.SpotifyTokenView.as_view(), name="spotify-token"),
1212
path("spotify/search/", views.SpotifySearchView.as_view(), name="spotify-search"),
13+
path("watchlist/", views.WatchlistView.as_view(), name="watchlist"),
14+
path("watchlist/<int:pk>/", views.WatchlistItemDetailView.as_view(), name="watchlist-detail"),
1315
]

0 commit comments

Comments
 (0)