Skip to content

Commit 07c9310

Browse files
committed
implemented backend logic for retrieving and updating a user profile
1 parent 48cc212 commit 07c9310

File tree

9 files changed

+158
-20
lines changed

9 files changed

+158
-20
lines changed

csm_web/frontend/src/components/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ const App = () => {
4141
<Route path="matcher/*" element={<EnrollmentMatcher />} />
4242
<Route path="policies/*" element={<Policies />} />
4343
<Route path="export/*" element={<DataExport />} />
44+
{
45+
// TODO: add route for profiles (/user/:id/* element = {UserProfile})
46+
}
4447
<Route path="*" element={<NotFound />} />
4548
</Route>
4649
</Routes>

csm_web/frontend/src/utils/queries/base.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const useProfiles = (): UseQueryResult<Profile[], Error> => {
3535
return queryResult;
3636
};
3737

38+
// TODO: move to new queries/users.tsx
3839
/**
3940
* Hook to get the user's info.
4041
*/
@@ -57,6 +58,7 @@ export const useUserInfo = (): UseQueryResult<RawUserInfo, Error> => {
5758
return queryResult;
5859
};
5960

61+
// TODO: move to new queries/users.tsx
6062
/**
6163
* Hook to get a list of all user emails.
6264
*/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.7 on 2024-07-20 05:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("scheduler", "0032_word_of_the_day"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="user",
14+
name="bio",
15+
field=models.CharField(default="", max_length=500),
16+
),
17+
migrations.AddField(
18+
model_name="user",
19+
name="pronouns",
20+
field=models.CharField(default="", max_length=20),
21+
),
22+
migrations.AddField(
23+
model_name="user",
24+
name="pronunciation",
25+
field=models.CharField(default="", max_length=50),
26+
),
27+
]

csm_web/scheduler/models.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ def week_bounds(date):
5151
class User(AbstractUser):
5252
priority_enrollment = models.DateTimeField(null=True, blank=True)
5353

54+
pronouns = models.CharField(max_length=20, default="", blank=True)
55+
pronunciation = models.CharField(max_length=50, default="", blank=True)
56+
# profile_pic = models.ImageField(upload_to='profiles/', null=True, blank=True)
57+
# if profile picture is implemented
58+
bio = models.CharField(max_length=500, default="", blank=True)
59+
5460
def can_enroll_in_course(self, course, bypass_enrollment_time=False):
5561
"""Determine whether this user is allowed to enroll in the given course."""
5662
# check restricted first
@@ -260,19 +266,15 @@ def save(self, *args, **kwargs):
260266
):
261267
if settings.DJANGO_ENV != settings.DEVELOPMENT:
262268
logger.info(
263-
(
264-
"<SectionOccurrence> SO automatically created for student"
265-
" %s in course %s for date %s"
266-
),
269+
"<SectionOccurrence> SO automatically created for student"
270+
" %s in course %s for date %s",
267271
self.user.email,
268272
course.name,
269273
now.date(),
270274
)
271275
logger.info(
272-
(
273-
"<Attendance> Attendance automatically created for student"
274-
" %s in course %s for date %s"
275-
),
276+
"<Attendance> Attendance automatically created for student"
277+
" %s in course %s for date %s",
276278
self.user.email,
277279
course.name,
278280
now.date(),

csm_web/scheduler/serializers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,16 @@ class Meta:
167167
class UserSerializer(serializers.ModelSerializer):
168168
class Meta:
169169
model = User
170-
fields = ("id", "email", "first_name", "last_name", "priority_enrollment")
170+
fields = (
171+
"id",
172+
"email",
173+
"first_name",
174+
"last_name",
175+
"priority_enrollment",
176+
"bio",
177+
"pronunciation",
178+
"pronouns",
179+
)
171180

172181

173182
class ProfileSerializer(serializers.Serializer): # pylint: disable=abstract-method

csm_web/scheduler/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
urlpatterns = router.urls
1616

1717
urlpatterns += [
18-
path("userinfo/", views.userinfo, name="userinfo"),
18+
path("user_info/", views.user_info, name="user_info"),
19+
path("user/<int:pk>/", views.user_retrieve, name="user_retrieve"),
20+
path("user/<int:pk>/update/", views.user_update, name="user_update"),
1921
path("matcher/active/", views.matcher.active),
2022
path("matcher/<int:pk>/slots/", views.matcher.slots),
2123
path("matcher/<int:pk>/preferences/", views.matcher.preferences),

csm_web/scheduler/views/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
from .section import SectionViewSet
77
from .spacetime import SpacetimeViewSet
88
from .student import StudentViewSet
9-
from .user import UserViewSet, userinfo
9+
from .user import UserViewSet, user_info, user_retrieve, user_update

csm_web/scheduler/views/user.py

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,123 @@
1-
from rest_framework.exceptions import PermissionDenied
2-
from rest_framework.response import Response
31
from rest_framework import status
42
from rest_framework.decorators import api_view
3+
from rest_framework.exceptions import PermissionDenied
4+
from rest_framework.response import Response
5+
from scheduler.serializers import UserSerializer
56

7+
from ..models import Coordinator, Mentor, Student, User
68
from .utils import viewset_with
7-
from ..models import Coordinator, User
8-
from scheduler.serializers import UserSerializer
9+
10+
# can create pytest to test this
911

1012

1113
class UserViewSet(*viewset_with("list")):
1214
serializer_class = None
1315
queryset = User.objects.all()
1416

1517
def list(self, request):
18+
"""
19+
Lists the emails of all users in the system. Only accessible by coordinators and superusers.
20+
"""
1621
if not (
17-
request.user.is_superuser
18-
or Coordinator.objects.filter(user=request.user).exists()
22+
# request.user.is_superuser or
23+
Coordinator.objects.filter(user=request.user).exists()
1924
):
2025
raise PermissionDenied(
2126
"Only coordinators and superusers may view the user email list"
2227
)
2328
return Response(self.queryset.order_by("email").values_list("email", flat=True))
2429

2530

31+
def has_permission(request_user, target_user):
32+
"""
33+
Returns True if the user has permission to access or edit the target user's profile
34+
"""
35+
# Does not account for users who are both mentors and students of different courses
36+
# need separation of concerns:
37+
# coordinators/mentor should only have coordinator/mentor access for their course
38+
# Check if request_user is a mentor
39+
if Mentor.objects.filter(user=request_user).exists():
40+
mentor_courses = Mentor.objects.filter(user=request_user).values_list(
41+
"course", flat=True
42+
)
43+
44+
if Student.objects.filter(user=target_user, course__in=mentor_courses).exists():
45+
return True
46+
if Mentor.objects.filter(user=target_user, course__in=mentor_courses).exists():
47+
return True
48+
49+
# Check if request_user is a student in the same course as target_user
50+
# Students in the same section can see each other
51+
if (
52+
Student.objects.filter(user=request_user).exists()
53+
and Student.objects.filter(user=target_user).exists()
54+
):
55+
request_user_courses = Student.objects.filter(user=request_user).values_list(
56+
"course", flat=True
57+
)
58+
target_user_courses = Student.objects.filter(user=target_user).values_list(
59+
"course", flat=True
60+
)
61+
62+
if set(request_user_courses) & set(target_user_courses):
63+
return True
64+
65+
# Coordinator access
66+
if Coordinator.objects.filter(
67+
user=request_user
68+
).exists(): # or if request_user.is_superuser
69+
return True
70+
71+
# Request user accessing their own profile
72+
if request_user == target_user:
73+
return True
74+
75+
return False
76+
77+
2678
@api_view(["GET"])
27-
def userinfo(request):
79+
def user_retrieve(request, pk):
2880
"""
29-
Get user info for request user
81+
Retrieve user profile. Only accessible by superusers and the user themselves.
82+
"""
83+
try:
84+
user = User.objects.get(pk=pk)
85+
except User.DoesNotExist:
86+
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
87+
88+
if not has_permission(request.user, user):
89+
raise PermissionDenied("You do not have permission to access this profile")
3090

31-
TODO: perhaps replace this with a viewset when we establish profiles
91+
serializer = UserSerializer(user)
92+
return Response(serializer.data)
93+
94+
95+
@api_view(["PUT"])
96+
def user_update(request, pk):
97+
"""
98+
Update user profile. Only accessible by Coordinators and the user themselves.
99+
"""
100+
try:
101+
user = User.objects.get(pk=pk)
102+
except User.DoesNotExist:
103+
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
104+
105+
if not (
106+
(request.user == user) or Coordinator.objects.filter(user=request.user).exists()
107+
):
108+
raise PermissionDenied("You do not have permission to edit this profile")
109+
110+
serializer = UserSerializer(user, data=request.data, partial=True)
111+
if serializer.is_valid():
112+
serializer.save()
113+
return Response(serializer.data)
114+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
115+
116+
117+
@api_view(["GET"])
118+
def user_info(request):
119+
"""
120+
Get user info for request user
32121
"""
33122
serializer = UserSerializer(request.user)
34123
return Response(serializer.data, status=status.HTTP_200_OK)

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ services:
5252
source: ./
5353
target: /opt/csm_web
5454
read_only: true
55+
# output from migrations
56+
- type: bind
57+
source: ./csm_web/scheduler/migrations/
58+
target: /opt/csm_web/csm_web/scheduler/migrations/
5559
depends_on:
5660
postgres:
5761
condition: service_healthy

0 commit comments

Comments
 (0)