Skip to content

Commit 62b906c

Browse files
committed
Create API for getting information on all current teachers. Create a separate permission for accessing the API.
Fixes #1503
1 parent 686eb75 commit 62b906c

5 files changed

Lines changed: 184 additions & 0 deletions

File tree

course/api/tests.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.test import override_settings
2+
from django.contrib.auth.models import Permission
23
from rest_framework.test import APIClient
34

45
from course.models import CourseInstance
@@ -144,3 +145,85 @@ def test_put_course(self):
144145
t = map(lambda x: x.user.username, course.teachers)
145146
self.assertIn('staff', t)
146147
self.assertIn('newteacher', t)
148+
149+
def test_get_current_teachers(self):
150+
self.user.email = 'teacher1@example.com'
151+
self.user.save(update_fields=['email'])
152+
self.user1.email = 'teacher2@example.com'
153+
self.user1.save(update_fields=['email'])
154+
self.user2.email = 'pastteacher@example.com'
155+
self.user2.save(update_fields=['email'])
156+
157+
self.current_course_instance.add_teacher(self.user.userprofile)
158+
self.current_course_instance.add_teacher(self.user1.userprofile)
159+
self.future_course_instance.add_teacher(self.user1.userprofile)
160+
self.past_course_instance.add_teacher(self.user2.userprofile)
161+
162+
client = APIClient()
163+
client.force_authenticate(user=self.superuser)
164+
response = client.get('/api/v2/courses/current-teachers/')
165+
166+
self.assertEqual(response.status_code, 200)
167+
self.assertIn('count', response.data)
168+
self.assertIn('results', response.data)
169+
170+
results = response.data['results']
171+
self.assertEqual(response.data['count'], 2)
172+
self.assertEqual(len(results), 2)
173+
174+
emails = sorted(row['email'] for row in results)
175+
self.assertEqual(emails, ['teacher1@example.com', 'teacher2@example.com'])
176+
177+
def test_get_current_teachers_requires_permission(self):
178+
self.current_course_instance.add_teacher(self.user.userprofile)
179+
180+
client = APIClient()
181+
client.force_authenticate(user=self.user)
182+
response = client.get('/api/v2/courses/current-teachers/')
183+
self.assertEqual(response.status_code, 403)
184+
185+
def test_get_current_teachers_with_permission(self):
186+
self.current_course_instance.add_teacher(self.user1.userprofile)
187+
self.user1.email = 'teacher2@example.com'
188+
self.user1.save(update_fields=['email'])
189+
190+
permission = Permission.objects.get(
191+
content_type__app_label='course',
192+
codename='view_current_teachers',
193+
)
194+
self.user.user_permissions.add(permission)
195+
196+
client = APIClient()
197+
client.force_authenticate(user=self.user)
198+
response = client.get('/api/v2/courses/current-teachers/')
199+
200+
self.assertEqual(response.status_code, 200)
201+
emails = [row['email'] for row in response.data['results']]
202+
self.assertIn('teacher2@example.com', emails)
203+
204+
def test_get_current_teachers_ended_within_days(self):
205+
self.user.email = 'teacher1@example.com'
206+
self.user.save(update_fields=['email'])
207+
self.user2.email = 'pastteacher@example.com'
208+
self.user2.save(update_fields=['email'])
209+
210+
self.current_course_instance.add_teacher(self.user.userprofile)
211+
self.past_course_instance.add_teacher(self.user2.userprofile)
212+
213+
client = APIClient()
214+
client.force_authenticate(user=self.superuser)
215+
response = client.get('/api/v2/courses/current-teachers/?ended_within_days=365')
216+
217+
self.assertEqual(response.status_code, 200)
218+
emails = sorted(row['email'] for row in response.data['results'])
219+
self.assertEqual(emails, ['pastteacher@example.com', 'teacher1@example.com'])
220+
221+
def test_get_current_teachers_ended_within_days_invalid(self):
222+
client = APIClient()
223+
client.force_authenticate(user=self.superuser)
224+
225+
response = client.get('/api/v2/courses/current-teachers/?ended_within_days=abc')
226+
self.assertEqual(response.status_code, 400)
227+
228+
response = client.get('/api/v2/courses/current-teachers/?ended_within_days=-1')
229+
self.assertEqual(response.status_code, 400)

course/api/views.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
)
4646
from ..permissions import (
4747
JWTInstanceWritePermission,
48+
CanViewCurrentTeachersPermission,
4849
OnlyCourseTeacherPermission,
4950
IsCourseAdminOrUserObjIsSelf,
5051
OnlyEnrolledStudentOrCourseStaffPermission,
@@ -116,6 +117,13 @@ class CourseViewSet(ListSerializerMixin,
116117
117118
* `subject`: email subject
118119
* `message`: email body
120+
121+
`GET /courses/current-teachers/`:
122+
returns users who are teachers in at least one currently running course.
123+
Optional query parameter:
124+
125+
* `ended_within_days`: include teachers from courses that ended within
126+
the given number of days (and ongoing/future courses). Example: `365`.
119127
"""
120128
lookup_url_kwarg = 'course_id'
121129
lookup_value_regex = REGEX_INT
@@ -142,6 +150,65 @@ def get_serializer_class(self):
142150
return CourseWriteSerializer
143151
return super().get_serializer_class()
144152

153+
@action(
154+
detail=False,
155+
methods=['get'],
156+
url_path='current-teachers',
157+
url_name='current-teachers',
158+
get_permissions=lambda: [CanViewCurrentTeachersPermission()],
159+
)
160+
def current_teachers(self, request, *args, **kwargs):
161+
now = timezone.now()
162+
ended_within_days = request.query_params.get('ended_within_days', '').strip()
163+
164+
course_filter = Q(
165+
course_instance__starting_time__lte=now,
166+
course_instance__ending_time__gte=now,
167+
)
168+
if ended_within_days:
169+
try:
170+
days = int(ended_within_days)
171+
except ValueError as exc:
172+
raise ParseError('"ended_within_days" must be an integer.') from exc
173+
if days < 0:
174+
raise ParseError('"ended_within_days" must be a non-negative integer.')
175+
course_filter = Q(course_instance__ending_time__gte=now - datetime.timedelta(days=days))
176+
177+
teacher_rows = (
178+
Enrollment.objects
179+
.filter(
180+
course_filter,
181+
role=Enrollment.ENROLLMENT_ROLE.TEACHER,
182+
status=Enrollment.ENROLLMENT_STATUS.ACTIVE,
183+
)
184+
.values(
185+
'user_profile__user_id',
186+
'user_profile__user__username',
187+
'user_profile__user__email',
188+
'user_profile__user__first_name',
189+
'user_profile__user__last_name',
190+
)
191+
.distinct()
192+
.order_by('user_profile__user_id')
193+
)
194+
195+
results = [
196+
{
197+
'id': row['user_profile__user_id'],
198+
'username': row['user_profile__user__username'],
199+
'email': row['user_profile__user__email'],
200+
'first_name': row['user_profile__user__first_name'],
201+
'last_name': row['user_profile__user__last_name'],
202+
}
203+
for row in teacher_rows
204+
if row['user_profile__user__email']
205+
]
206+
207+
return Response({
208+
'count': len(results),
209+
'results': results,
210+
})
211+
145212
# get_permissions lambda overwrites the normal version used for the above methods
146213
@action(detail=True, methods=["post"], get_permissions=lambda: [JWTInstanceWritePermission()])
147214
def notify_update(self, request, *args, **kwargs):
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.2.18 on 2026-02-18
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('course', '0063_courseinstance_group_work_allowed'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='courseinstance',
15+
options={
16+
'verbose_name': 'MODEL_NAME_COURSE_INSTANCE',
17+
'verbose_name_plural': 'MODEL_NAME_COURSE_INSTANCE_PLURAL',
18+
'permissions': (('view_current_teachers', 'Can view current teachers API endpoint'),),
19+
},
20+
),
21+
]

course/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,9 @@ class Meta:
801801
verbose_name = _('MODEL_NAME_COURSE_INSTANCE')
802802
verbose_name_plural = _('MODEL_NAME_COURSE_INSTANCE_PLURAL')
803803
unique_together = ("course", "url")
804+
permissions = (
805+
('view_current_teachers', 'Can view current teachers API endpoint'),
806+
)
804807

805808
def __str__(self):
806809
return "{}: {}".format(str(self.course), self.instance_name)

course/permissions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,16 @@ def has_object_permission(self, request: HttpRequest, view: Any, module: CourseM
271271
return True
272272

273273

274+
class CanViewCurrentTeachersPermission(Permission):
275+
message = _('You do not have permission to access current teachers endpoint.')
276+
277+
def has_permission(self, request, view):
278+
return (
279+
request.user.is_authenticated
280+
and request.user.has_perm('course.view_current_teachers')
281+
)
282+
283+
274284
CourseModulePermission = CourseModulePermissionBase | JWTInstanceReadPermission
275285

276286

0 commit comments

Comments
 (0)