Skip to content

Commit 5f0a58d

Browse files
Merge branch 'main' into unit-tests-for-chat
2 parents b403fa2 + e26410d commit 5f0a58d

File tree

219 files changed

+52619
-30
lines changed

Some content is hidden

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

219 files changed

+52619
-30
lines changed

backend/apps/chat/consumers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ async def receive(self, text_data):
3434
text_data_json = json.loads(text_data)
3535
if 'message' in text_data_json:
3636
message = text_data_json['message']
37+
3738
try:
3839
# Try to save the message
3940
username, id, timestamp = await self.save_message(self.user_id, self.room_name, message)
@@ -58,6 +59,7 @@ async def receive(self, text_data):
5859
await self.send(text_data=json.dumps({
5960
"error": f"Chat room '{self.room_name}' not found. Message not saved."
6061
}))
62+
6163
# Typing status is received
6264
elif 'typing' in text_data_json:
6365
typing = text_data_json['typing']

backend/apps/chat/tests.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
from rest_framework import status
44
from rest_framework.test import APITestCase, APIClient
55
from .models import Chat, Message
6+
from apps.chat.consumers import ChatConsumer
7+
8+
def test_chat_consumer_init():
9+
consumer = ChatConsumer()
10+
assert consumer is not None
611

712
class ChatAPITestCase(APITestCase):
813

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.1.6 on 2025-04-23 20:47
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('search', '0002_alter_subject_table'),
10+
('users', '0010_profile_date_of_birth_profile_phone_number_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='studentprofile',
16+
name='emergency_contact_name',
17+
field=models.CharField(default='Unknown', max_length=100),
18+
),
19+
migrations.AlterField(
20+
model_name='studentprofile',
21+
name='emergency_contact_phone_number',
22+
field=models.CharField(default='1234567890', max_length=15),
23+
),
24+
migrations.AlterField(
25+
model_name='studentprofile',
26+
name='preferred_subjects',
27+
field=models.ManyToManyField(blank=True, to='search.subject'),
28+
),
29+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.6 on 2025-04-23 20:52
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('users', '0011_alter_studentprofile_emergency_contact_name_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='tutorprofile',
15+
name='hourly_rate',
16+
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=6),
17+
),
18+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.6 on 2025-04-23 21:28
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+
('users', '0012_alter_tutorprofile_hourly_rate'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='studentprofile',
16+
name='parent_profile',
17+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.parentprofile'),
18+
),
19+
]

backend/apps/users/models.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class TutorProfile(models.Model):
5050
state = models.CharField(max_length=2, default="NA")
5151

5252
bio = models.TextField(blank=True)
53-
hourly_rate = models.DecimalField(max_digits=6, decimal_places=2)
53+
hourly_rate = models.DecimalField(max_digits=6, decimal_places=2, default= 0.00)
5454

5555
subjects = models.ManyToManyField(to="search.Subject")
5656
rating = models.DecimalField(max_digits=2, decimal_places=1, default=1.0)
@@ -99,12 +99,21 @@ def __str__(self):
9999
class StudentProfile(models.Model):
100100
profile = models.OneToOneField(Profile, on_delete=models.CASCADE)
101101
# also including a reference to the parent profile
102-
parent_profile = models.OneToOneField(ParentProfile, on_delete=models.CASCADE)
102+
parent_profile = models.ForeignKey(ParentProfile, on_delete=models.CASCADE)
103103
# https://stackoverflow.com/questions/849142/how-to-limit-the-maximum-value-of-a-numeric-field-in-a-django-model
104104
grade_level = models.IntegerField(default=1, validators=[MaxValueValidator(12),MinValueValidator(1)])
105-
preferred_subjects = models.ManyToManyField(to="search.Subject")
106-
emergency_contact_name = models.CharField(max_length=100)
107-
emergency_contact_phone_number = models.CharField(max_length=15)
105+
preferred_subjects = models.ManyToManyField(
106+
to="search.Subject",
107+
blank=True
108+
)
109+
emergency_contact_name = models.CharField(
110+
max_length=100,
111+
default = "Unknown"
112+
)
113+
emergency_contact_phone_number = models.CharField(
114+
max_length=15,
115+
default = "1234567890"
116+
)
108117

109118
def clean(self):
110119
if self.profile.role != self.profile.STUDENT:

backend/apps/users/tests.py

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,117 @@
11
from django.contrib.auth.models import User
22
from django.test import TestCase
3-
from django.urls import reverse
4-
from rest_framework.test import APIClient
3+
from rest_framework.test import APIClient, APITestCase
54
from rest_framework import status
65
from .models import Profile, StudentProfile, TutorProfile, ParentProfile
76
from rest_framework_simplejwt.tokens import RefreshToken
87

8+
# https://chatgpt.com/share/6804498e-2dfc-8005-8b02-d78afdc354ae
9+
class UserRegistrationTests(APITestCase):
10+
def test_register_student_profile(self):
11+
parent_profile = User.objects.create_user(username="parentuser", password="pass123")
12+
parent_profile_instance = ParentProfile.objects.create(profile=Profile.objects.create(user=parent_profile, role=Profile.PARENT))
13+
14+
url = "/users/register/" # make sure you have this in urls.py
15+
data = {
16+
"country": "US",
17+
"username": "studentuser",
18+
"email": "student@example.com",
19+
"firstName": "Student",
20+
"lastName": "User",
21+
"password": "testpassword",
22+
"role": 3,
23+
"parent_profile": parent_profile_instance.id,
24+
"grade_level": 5,
25+
"preferred_subjects": [],
26+
"emergency_contact_name": "Parent User",
27+
"emergency_contact_phone_number": "5555555555"
28+
}
29+
response = self.client.post(url, data, format='json')
30+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
31+
self.assertTrue(User.objects.filter(username="studentuser").exists())
32+
self.assertTrue(Profile.objects.filter(user__username="studentuser").exists())
33+
self.assertTrue(StudentProfile.objects.filter(profile__user__username="studentuser").exists())
34+
35+
def test_register_tutor_profile(self):
36+
url = "/users/register/"
37+
data = {
38+
"country": "US",
39+
"username": "tutoruser",
40+
"email": "tutor@example.com",
41+
"firstName": "Tutor",
42+
"lastName": "User",
43+
"password": "testpassword",
44+
"role": 1,
45+
"city": "New York",
46+
"state": "NY",
47+
"bio": "I love teaching.",
48+
"hourly_rate": "40.00"
49+
}
50+
response = self.client.post(url, data, format='json')
51+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
52+
self.assertTrue(User.objects.filter(username="tutoruser").exists())
53+
self.assertTrue(TutorProfile.objects.filter(profile__user__username="tutoruser").exists())
54+
55+
def test_register_parent_profile(self):
56+
url = "/users/register/"
57+
data = {
58+
"country": "US",
59+
"username": "parentuser2",
60+
"email": "parent2@example.com",
61+
"firstName": "Parent",
62+
"lastName": "User",
63+
"password": "testpassword",
64+
"role": 2,
65+
}
66+
response = self.client.post(url, data, format='json')
67+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
68+
self.assertTrue(User.objects.filter(username="parentuser2").exists())
69+
self.assertTrue(ParentProfile.objects.filter(profile__user__username="parentuser2").exists())
70+
71+
72+
class UserProfileViewTests(APITestCase):
73+
def setUp(self):
74+
# Student user
75+
student_user = User.objects.create_user(username="student", password="password")
76+
student_profile = Profile.objects.create(user=student_user, role=Profile.STUDENT)
77+
parent_user = User.objects.create_user(username="parent", password="password")
78+
parent_profile = Profile.objects.create(user=parent_user, role=Profile.PARENT)
79+
parent_profile_instance = ParentProfile.objects.create(profile=parent_profile)
80+
StudentProfile.objects.create(
81+
profile=student_profile,
82+
parent_profile=parent_profile_instance,
83+
grade_level=6,
84+
emergency_contact_name="Test Parent",
85+
emergency_contact_phone_number="1112223333"
86+
)
87+
self.student_user = student_user
88+
# api
89+
self.client = APIClient()
90+
self.login_url = "/users/login/"
91+
response = self.client.post(self.login_url, {"username": "student", "password": "password"})
92+
self.assertEqual(response.status_code, status.HTTP_200_OK)
93+
self.refresh_token = response.json().get("refreshToken")
94+
self.access_token = response.json().get("accessToken")
95+
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.access_token}")
96+
97+
def test_get_student_profile(self):
98+
url = "/users/user-profile/" # replace with your actual path name
99+
response = self.client.get(url)
100+
self.assertEqual(response.status_code, status.HTTP_200_OK)
101+
self.assertIn('grade_level', response.data)
102+
103+
def test_patch_student_profile(self):
104+
url = "/users/user-profile/" # replace with your actual path name
105+
response = self.client.patch(url, {'grade_level': 8}, format='json')
106+
self.assertEqual(response.status_code, status.HTTP_200_OK)
107+
self.assertEqual(response.data['grade_level'], 8)
108+
109+
def test_profile_not_found(self):
110+
Profile.objects.filter(user=self.student_user).delete() # force no profile
111+
url = "/users/user-profile/"
112+
response = self.client.get(url)
113+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
114+
9115
class UserAPITestCase(TestCase):
10116
def setUp(self):
11117
"""Set up test data and API client."""

backend/apps/users/views.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from apps.users.models import Profile, TutorProfile, ParentProfile, StudentProfile
99
from apps.uploads.models import UploadRecord, ProfilePicture
1010
from rest_framework import status
11+
from apps.search.models import Subject
1112
from rest_framework.response import Response
1213
from rest_framework.permissions import AllowAny, IsAuthenticated
1314
from rest_framework_simplejwt.tokens import RefreshToken
@@ -63,14 +64,15 @@ def logout_view(request):
6364
@permission_classes([AllowAny])
6465
def register_profile(request):
6566
country = request.data["country"]
66-
username = request.data["displayName"]
67+
username = request.data["username"]
6768
email = request.data["email"]
6869
first_name = request.data["firstName"]
6970
last_name = request.data["lastName"]
7071
password = request.data["password"]
71-
# TODO: replace "1" with "role" once that is handled by the frontend
72-
# role = request.data["role"] # Get the selected role
73-
role = Profile.STUDENT
72+
role = request.data.get("role") # Get the selected role
73+
if not role:
74+
role = Profile.TUTOR
75+
7476
# Create user
7577
user = User.objects.create_user(
7678
username=username,
@@ -81,8 +83,6 @@ def register_profile(request):
8183
)
8284
# Create associated Profile
8385
profile = Profile.objects.create(user, role)
84-
85-
8686
image=request.data.get("image") #For now, get an optional image
8787

8888
# Store optional profile picture
@@ -97,31 +97,48 @@ def register_profile(request):
9797

9898
# Create Tutor Profile if role is Tutor
9999
if int(profile.role) == Profile.TUTOR:
100-
city=request.data["city"]
101-
state=request.data["state"]
102-
bio=request.data["bio"]
103-
hourly_rate=request.data["hourly_rate"]
100+
city=request.data.get("city") or "Unknown"
101+
state=request.data.get("state") or "NA"
102+
bio=request.data.get("bio")
103+
if not bio:
104+
bio = ""
105+
hourly_rate=request.data.get("hourly_rate") or 0.00
104106
tutor = TutorProfile.objects.create(profile=profile, city=city, state=state, bio=bio, hourly_rate=hourly_rate)
105107
# otherwise, check if parent
106108
elif int(profile.role) == Profile.PARENT:
107109
parent = ParentProfile.objects.create(profile=profile)
108110
# otherwise, student
109111
else:
110-
parent_profile = request.data["parent_profile"]
111-
grade_level = request.data["grade_level"]
112-
preferred_subjects = request.data["preferred_subjects"]
113-
emergency_contact_name = request.data["emergency_contact_name"]
114-
emergency_contact_phone_number = request.data["emergency_contact_phone_number"]
115-
parent_profile_id = request.data["parent_profile"]
116-
parent_profile_instance = ParentProfile.objects.get(id=parent_profile_id)
112+
parent_profile = request.data.get("parent_profile")
113+
grade_level = request.data.get("grade_level")
114+
preferred_subjects = request.data.get("preferred_subjects")
115+
grade_level = request.data.get("grade_level")
116+
if not grade_level:
117+
grade_level = 1 # same as the model default
118+
119+
emergency_contact_name = request.data.get("emergency_contact_name") or "Unknown"
120+
emergency_contact_phone_number = request.data.get("emergency_contact_phone_number") or "1234567890"
121+
parent_profile_id = request.data.get("parent_profile")
122+
if parent_profile_id:
123+
parent_profile_instance = ParentProfile.objects.get(id=parent_profile_id)
124+
125+
else:
126+
parent_profile_instance = ParentProfile.objects.get(id=1) # for now
127+
117128
student = StudentProfile.objects.create(
118129
profile=profile,
119130
parent_profile=parent_profile_instance,
120131
grade_level = grade_level,
121-
preferred_subjects = preferred_subjects,
122132
emergency_contact_name = emergency_contact_name,
123133
emergency_contact_phone_number = emergency_contact_phone_number
124134
)
135+
# For now, just to get sign up to work without full frontend + backend connection
136+
if not preferred_subjects:
137+
default_subjects = Subject.objects.filter(title__in=["Biology"])
138+
student.preferred_subjects.set(default_subjects)
139+
else:
140+
student.preferred_subjects.set(preferred_subjects)
141+
125142
return Response({"message": "User registered successfully"}, status=status.HTTP_201_CREATED)
126143

127144
@api_view(['POST'])

backend/backend/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
'django.contrib.contenttypes',
6060
'django.contrib.sessions',
6161
'django.contrib.messages',
62+
'django_extensions',
6263
"daphne", #daphne has to be before django.contrib.staticfiles
6364
'django.contrib.staticfiles',
6465
"apps.api", # our api app

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pytest-django==4.8.0
2323
pytest-asyncio==0.23.5
2424
factory-boy==3.3.0
2525
coverage==7.5.0
26+
django-extensions==4.1
2627

2728
# Dev dependencies
2829
pytest-cov==6.1.1

0 commit comments

Comments
 (0)