Skip to content

Commit 1849da0

Browse files
Merge pull request #133 from UNLV-CS472-672/Student-view-dashboard-functions
5th PR: Add Student Dashboard, Assignments, and File Upload Features
2 parents 8d77b64 + 91831af commit 1849da0

File tree

2,715 files changed

+142109
-1145
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

2,715 files changed

+142109
-1145
lines changed

.DS_Store

2 KB
Binary file not shown.

backend/apps/search/serializers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ class Meta:
1616

1717
def get_image_url(self, result_data):
1818
# Ensure that the profile and profile_picture exist before accessing them
19-
if hasattr(result_data, "profile") and hasattr(result_data.profile, "upload_record"):
20-
return UploadRecord.objects.build_url(result_data.profile.upload_record)
19+
if hasattr(result_data, "profile") and hasattr(result_data.profile, "profile_picture") and hasattr(result_data.profile.profile_picture, "upload_record"):
20+
return UploadRecord.objects.build_url(result_data.profile.profile_picture.upload_record)
2121
return None # Return None if there's no profile picture
2222

2323
def get_subjects(self, result_data):

backend/apps/submissions/tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def setUp(self):
2929
"version": 1,
3030
"asset_id": "sample_asset_id"
3131
}
32-
self.upload_record = UploadRecord.objects.create(upload_data=self.upload_data, profile=self.profile)
32+
self.upload_record = UploadRecord.objects.create(upload_data=self.upload_data)
3333

3434
# Create submission
3535
self.submission = Submissions.objects.create(
@@ -426,7 +426,7 @@ def setUp(self):
426426
"version": 1,
427427
"asset_id": "sample_asset_id"
428428
}
429-
self.file_record = UploadRecord.objects.create(self.upload_data, self.profile)
429+
self.file_record = UploadRecord.objects.create(self.upload_data)
430430
self.quiz_submission = QuizSubmissions.objects.create(submission=self.submission)
431431

432432
self.login_url = "/users/login/"

backend/apps/uploads/managers.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from cloudinary import CloudinaryImage
66

77
class ProfilePictureManager(models.Manager):
8-
def create(self, upload):
8+
def create(self, profile):
99
# Create a new UploadRecord instance with the provided data
1010
profile_picture = self.model(
11-
upload=upload
11+
profile=profile
1212
)
1313
# Save the instance to the database
1414
profile_picture.save()
@@ -26,7 +26,11 @@ def delete_upload(self, cloudinary_public_id):
2626
result = cloudinary.uploader.destroy(cloudinary_public_id, invalidate = True)
2727
return result
2828

29-
def create(self, upload_data, profile):
29+
def add_profile_picture(self, upload_record, profile_picture):
30+
upload_record.profile_picture = profile_picture
31+
upload_record.save(update_fields=['profile_picture'])
32+
33+
def create(self, upload_data):
3034
# Replace 'Z' with '+00:00' for compatibility with fromisoformat
3135
compatible_created_at = upload_data['created_at'].replace('Z', '+00:00')
3236
# Convert to datetime object
@@ -42,7 +46,6 @@ def create(self, upload_data, profile):
4246
cloudinary_public_id=upload_data["public_id"],
4347
version=upload_data["version"],
4448
asset_id=upload_data["asset_id"],
45-
profile=profile
4649
)
4750
# Save the instance to the database
4851
upload_record.save()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 5.1.6 on 2025-04-11 07:35
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('uploads', '0007_alter_uploadrecord_profile'),
11+
('users', '0009_alter_tutorprofile_rating'),
12+
]
13+
14+
operations = [
15+
migrations.RemoveField(
16+
model_name='profilepicture',
17+
name='upload',
18+
),
19+
migrations.RemoveField(
20+
model_name='uploadrecord',
21+
name='profile',
22+
),
23+
migrations.AddField(
24+
model_name='profilepicture',
25+
name='profile',
26+
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile_picture', to='users.profile'),
27+
),
28+
migrations.AddField(
29+
model_name='uploadrecord',
30+
name='profile_picture',
31+
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='upload_record', to='uploads.profilepicture'),
32+
),
33+
]

backend/apps/uploads/models.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@
55
from cloudinary.models import CloudinaryField
66
from django.core.exceptions import ValidationError
77

8+
# A model that acts as a container for the profile picture of user
9+
class ProfilePicture(models.Model):
10+
profile = models.OneToOneField(Profile, on_delete=models.CASCADE, related_name = "profile_picture", null = True) #changed
11+
# Link the custom manager to the model
12+
objects = ProfilePictureManager()
13+
14+
def save(self, *args, **kwargs):
15+
# Always validate before saving
16+
self.full_clean()
17+
super().save(*args, **kwargs)
18+
19+
def __str__(self):
20+
first_name = self.profile.user.first_name
21+
return f"{first_name}'s profile picture"
22+
823
# https://blog.nonstopio.com/well-handling-of-cloudinary-with-python-drf-api-28271575e21f
924
# Model that acts a container for all upload related information
1025
class UploadRecord(models.Model):
@@ -21,28 +36,12 @@ class UploadRecord(models.Model):
2136
version = models.PositiveBigIntegerField()
2237
asset_id = models.CharField(max_length=255)
2338

24-
profile = models.OneToOneField(Profile, on_delete=models.CASCADE, related_name='upload_record')
25-
2639
# default = 1 for first user (for now)
2740
description = models.TextField(default="", blank=True, null=False)
41+
profile_picture = models.OneToOneField(ProfilePicture, on_delete=models.CASCADE, related_name='upload_record', null = True)
2842

2943
# Link the custom manager to the model
3044
objects = UploadRecordManager()
3145

3246
def __str__(self):
33-
return self.file_name
34-
35-
# A model that acts as a container for the profile picture of user
36-
class ProfilePicture(models.Model):
37-
upload = models.OneToOneField(UploadRecord, on_delete=models.CASCADE)
38-
39-
# Link the custom manager to the model
40-
objects = ProfilePictureManager()
41-
42-
def save(self, *args, **kwargs):
43-
# Always validate before saving
44-
self.full_clean()
45-
super().save(*args, **kwargs)
46-
47-
def __str__(self):
48-
return self.upload.file_name
47+
return self.file_name

backend/apps/uploads/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def get_file_url(self, upload):
1515
return UploadRecord.objects.build_url(upload)
1616

1717
class UploadListSerializer(serializers.ModelSerializer):
18+
1819
class Meta:
1920
model = UploadRecord
20-
fields = ['file_name', 'file_format', 'cloudinary_public_id']
21+
fields = ['file_name', 'file_format', 'cloudinary_public_id']

backend/apps/uploads/tests.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from apps.users.models import Profile
66
from django.contrib.auth import get_user_model
77
from .serializers import UploadListSerializer
8+
from rest_framework_simplejwt.tokens import RefreshToken
89

910
class UploadRecordViewTest(APITestCase):
1011
def setUp(self):
@@ -17,8 +18,6 @@ def setUp(self):
1718
self.version = 1234567890
1819
self.asset_id = "test-asset-id"
1920

20-
self.url = reverse('upload-list')
21-
2221
self.fake_user = get_user_model().objects.create_user(
2322
username='testuser',
2423
password='password',
@@ -27,17 +26,25 @@ def setUp(self):
2726
email='testuser@example.com'
2827
)
2928
self.profile = Profile.objects.create(self.fake_user, 2)
29+
self.token = self.get_jwt_token(self.fake_user)
30+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.token}')
31+
32+
def get_jwt_token(self, user):
33+
refresh = RefreshToken.for_user(user)
34+
return str(refresh.access_token)
3035

3136
# Ensure that if there are no uploads,
3237
# the API returns a 404 error with the proper message.
3338
def test_get_uploads_empty(self):
34-
response = self.client.get(self.url)
39+
url = reverse('upload-list')
40+
response = self.client.get(url)
3541
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
3642
self.assertEqual(response.data, {'error': 'No uploads found'})
3743

3844
# Test that the API returns a 200 status and
3945
# the correct serialized data when uploads exist.
4046
def test_get_uploads_success(self):
47+
url = reverse('upload-list')
4148
# Create an example UploadRecord instance.
4249
upload_record = UploadRecord.objects.create(
4350
upload_data={
@@ -50,11 +57,10 @@ def test_get_uploads_success(self):
5057
"version": self.version,
5158
"asset_id": self.asset_id,
5259
},
53-
profile=self.profile
5460
)
5561

5662
# Perform the GET request
57-
response = self.client.get(self.url)
63+
response = self.client.get(url)
5864
self.assertEqual(response.status_code, status.HTTP_200_OK)
5965

6066
# Serialize the created object using the serializer

backend/apps/uploads/views.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# Create your views here.
1616

1717
class UploadDetailView(APIView):
18-
permission_classes = [] # Debug only: No authentication required
18+
permission_classes = [IsAuthenticated]
1919
# Specifies that the view should only accept JSON-formatted request bodies.
2020
parser_classes = (
2121
JSONParser,
@@ -89,7 +89,7 @@ def patch(self, request, cloudinary_public_id):
8989

9090

9191
class UploadListView(APIView):
92-
permission_classes = [] # Debug only: No authentication required
92+
permission_classes = [IsAuthenticated]
9393
# Specifies that the view can accept both multipart form data
9494
# and JSON-formatted request bodies.
9595
parser_classes = (
@@ -101,17 +101,12 @@ class UploadListView(APIView):
101101
# Uploading a new file
102102
def post(self, request):
103103
# request to get user
104-
# user = request.user # once authentication is a thing
105-
106-
# Get the user model class (Debug only)
107-
User = apps.get_model(settings.AUTH_USER_MODEL)
108-
109-
# Get the first user in the database (Debug only)
110-
user = User.objects.first()
104+
user = request.user # Authenticated user object
105+
profile = user.profile
111106

112107
# Check if user exists
113-
if not user:
114-
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
108+
if not profile:
109+
return Response({'error': 'Profile not found'}, status=status.HTTP_404_NOT_FOUND)
115110

116111
# request to get file to be uploaded
117112
file = request.data.get('file')
@@ -124,7 +119,7 @@ def post(self, request):
124119
upload_data = UploadRecord.objects.upload(file)
125120

126121
# Use the manager method to save relevant metadata into database
127-
UploadRecord.objects.create(upload_data, user)
122+
UploadRecord.objects.create(upload_data)
128123

129124
return Response({'status': 'success'}, status=status.HTTP_200_OK)
130125
# Note: Tested this POST request by entering this into the command line

backend/apps/users/managers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,6 @@ def get_result_data(self, search_results):
8787
'hourly_rate',
8888
'state',
8989
'city',
90-
'profile__upload_record'
90+
'profile__profile_picture__upload_record' #changed
9191
)
9292
return search_results

0 commit comments

Comments
 (0)