Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added edx_when/rest_api/__init__.py
Empty file.
91 changes: 91 additions & 0 deletions edx_when/rest_api/v1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
### 📘 `GET /api/edx_when/v1/user-dates/`
### 📘 `GET /api/edx_when/v1/user-dates/{course_id}`

### Description
Retrieves user-specific dates for a specific course or all enrolled courses. in Open edX. Dates may include due dates, release dates, etc. Supports optional filtering.

---

### 🔐 Authentication
- Required: ✅ Yes
- Methods:
- `SessionAuthentication`
- `JwtAuthentication`

User must be authenticated and have access to the course.

---

### 📥 Path Parameters

| Name | Type | Required | Description |
|------------|--------|----------|---------------------------------|
| course_id | string | ❌ No | Course ID in URL-encoded format |

---

### 🧾 Query Parameters (optional)

| Name | Type | Description |
|-------------|--------|-------------------------------------------------------------------------|
| block_types | string | Comma-separated list of block types (e.g., `problem,html`) |
| block_keys | string | Comma-separated list of block keys (usage IDs or block identifiers) |
| date_types | string | Comma-separated list of date types (e.g., `start,due`) |

---

### ✅ Response (200 OK)

```json
{
"block-v1:edX+DemoX+2023+type@problem+block@123abc": "2025-07-01T12:00:00Z",
"block-v1:edX+DemoX+2023+type@video+block@456def": "2025-07-03T09:30:00Z"
}
```

- A dictionary where keys are block identifiers and values are ISO 8601 date strings.

---

### 🔒 Response Codes

| Code | Meaning |
|------|----------------------------------|
| 200 | Success |
| 401 | Unauthorized (not logged in) |
| 403 | Forbidden (no access to course) |

---

### 💡 Usage Example

#### Requests
```http
GET /api/edx_when/v1/user-dates/
```

```http
GET /api/edx_when/v1/user-dates/course-v1:edX+DemoX+2023
```

#### With Filters
```http
GET /api/edx_when/v1/user-dates/?block_types=problem,video&date_types=due
```

```http
GET /api/edx_when/v1/user-dates/course-v1:edX+DemoX+2023?block_types=problem,video&date_types=due
```

#### Curl Example
```bash
curl -X GET "https://your-domain.org/api/edx_when/v1/user-dates/?block_types=problem&date_types=due" \
-H "Authorization: Bearer <your_jwt_token>"
```

```bash
curl -X GET "https://your-domain.org/api/edx_when/v1/user-dates/course-v1:edX+DemoX+2023?block_types=problem&date_types=due" \
-H "Authorization: Bearer <your_jwt_token>"
```

---
Empty file.
Empty file.
155 changes: 155 additions & 0 deletions edx_when/rest_api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
Tests for the UserDatesView in the edx_when REST API.
"""

from datetime import datetime
from unittest.mock import patch

from django.urls import reverse
from django.contrib.auth.models import User
from rest_framework.test import APITestCase


class TestUserDatesView(APITestCase):
"""
Tests for UserDatesView.
"""

def setUp(self):
self.user = User.objects.create_user(username='testuser', password='testpass')
self.course_id = 'course-v1:TestOrg+TestCourse+TestRun'
self.url = reverse('edx_when:v1:user_dates', kwargs={'course_id': self.course_id})

@patch('edx_when.rest_api.v1.views.get_user_dates')
def test_get_user_dates_success(self, mock_get_user_dates):
"""
Test successful retrieval of user dates.
"""
mock_user_dates = {
('assignment_1', 'due'): datetime(2023, 12, 15, 23, 59, 59),
('quiz_1', 'due'): datetime(2023, 12, 20, 23, 59, 59),
}
mock_get_user_dates.return_value = mock_user_dates

self.client.force_authenticate(user=self.user)
response = self.client.get(self.url)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {
'assignment_1': datetime(2023, 12, 15, 23, 59, 59),
'quiz_1': datetime(2023, 12, 20, 23, 59, 59),
})
mock_get_user_dates.assert_called_once_with(
self.course_id,
self.user.id,
block_types=None,
block_keys=None,
date_types=None
)

@patch('edx_when.rest_api.v1.views.get_user_dates')
def test_get_user_dates_with_filters(self, mock_get_user_dates):
"""
Test retrieval of user dates with query parameter filters.
"""
mock_get_user_dates.return_value = {}

self.client.force_authenticate(user=self.user)
response = self.client.get(self.url, {
'block_types': 'assignment,quiz',
'block_keys': 'block1,block2',
'date_types': 'due,start'
})

self.assertEqual(response.status_code, 200)
mock_get_user_dates.assert_called_once_with(
self.course_id,
self.user.id,
block_types=['assignment', 'quiz'],
block_keys=['block1', 'block2'],
date_types=['due', 'start']
)

@patch('edx_when.rest_api.v1.views.get_user_dates')
def test_get_user_dates_empty_filters(self, mock_get_user_dates):
"""
Test that empty filter parameters are converted to None.
"""
mock_get_user_dates.return_value = {}

self.client.force_authenticate(user=self.user)
response = self.client.get(self.url, {
'block_types': '',
'block_keys': '',
'date_types': ''
})

self.assertEqual(response.status_code, 200)
mock_get_user_dates.assert_called_once_with(
self.course_id,
self.user.id,
block_types=None,
block_keys=None,
date_types=None
)

def test_get_user_dates_unauthenticated(self):
"""
Test that unauthenticated requests return 401.
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 401)

@patch('edx_when.rest_api.v1.views.get_user_dates')
def test_get_user_dates_empty_response(self, mock_get_user_dates):
"""
Test successful retrieval with empty user dates.
"""
mock_get_user_dates.return_value = {}

self.client.force_authenticate(user=self.user)
response = self.client.get(self.url)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {})

@patch('edx_when.rest_api.v1.views.get_user_dates')
def test_get_user_dates_multiple_courses(self, mock_get_user_dates):
"""
Test retrieval of user dates for multiple enrolled courses.
"""
with patch.object(self.user, 'courseenrollment_set') as mock_enrollment_set:
mock_enrollment_set.filter.return_value.values_list.return_value = [
'course-v1:TestOrg+Course1+Run1',
'course-v1:TestOrg+Course2+Run2'
]

mock_get_user_dates.side_effect = [
{('assignment_1', 'due'): datetime(2023, 12, 15, 23, 59, 59)},
{('quiz_1', 'due'): datetime(2023, 12, 20, 23, 59, 59)}
]

self.client.force_authenticate(user=self.user)
url = reverse('edx_when:v1:user_dates_no_course')
response = self.client.get(url)

self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data.keys()), 2)
self.assertEqual(response.data['assignment_1'], datetime(2023, 12, 15, 23, 59, 59))
self.assertEqual(response.data['quiz_1'], datetime(2023, 12, 20, 23, 59, 59))

self.assertEqual(mock_get_user_dates.call_count, 2)
mock_get_user_dates.assert_any_call(
'course-v1:TestOrg+Course1+Run1',
self.user.id,
block_types=None,
block_keys=None,
date_types=None
)
mock_get_user_dates.assert_any_call(
'course-v1:TestOrg+Course2+Run2',
self.user.id,
block_types=None,
block_keys=None,
date_types=None
)
16 changes: 16 additions & 0 deletions edx_when/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
URLs for edx_when REST API v1.
"""

from django.conf import settings
from django.urls import re_path

from . import views

urlpatterns = [
re_path(
r'user-dates/(?:{})?'.format(settings.COURSE_ID_PATTERN),
views.UserDatesView.as_view(),
name='user_dates',
),
]
84 changes: 84 additions & 0 deletions edx_when/rest_api/v1/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Views for the edx-when REST API v1.
"""

from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework.response import Response

from edx_when.api import get_user_dates


class UserDatesView(APIView):
"""
View to handle user dates.
"""

authentication_classes = (SessionAuthentication, JwtAuthentication)
permission_classes = (IsAuthenticated,)

def get(self, request, *args, **kwargs):
"""
**Use Cases**

Request user dates for a specific course or all enrolled courses.

**Example Requests**

GET /api/edx_when/v1/user-dates/
GET /api/edx_when/v1/user-dates/{course_id}

**Parameters:**

course_id: (optional, str) Course ID to get dates for. If not provided, returns dates for all enrolled courses.
block_types: (optional, str) Comma-separated list of block types to filter the dates.
block_keys: (optional, str) Comma-separated list of block keys to filter the dates.
date_types: (optional, str) Comma-separated list of date types to filter the dates.

**Response Values**

Body consists of the following fields:

* user_dates: (dict) A dictionary containing user-specific dates for the course(s).
* The keys are date identifiers and the values are the corresponding date values.

**Example Response**

{
"block1_due": "2023-10-01T12:00:00Z",
"block2_start": "2023-10-05T08:00:00
}

**Returns**

* 200 on success with user dates.
* 401 if the user is not authenticated.
* 403 if the user does not have permission to access the course.
"""

course_id = kwargs.get('course_id')
block_types = request.query_params.get('block_types', '').split(',')
block_keys = request.query_params.get('block_keys', '').split(',')
date_types = request.query_params.get('date_types', '').split(',')

enrolled_courses = (
[course_id]
if course_id
else request.user.courseenrollment_set.filter(is_active=True).values_list(
"course_id", flat=True
)
)
all_user_dates = {}

for enrolled_course_id in enrolled_courses:
user_dates = get_user_dates(
enrolled_course_id,
request.user.id,
block_types=block_types if block_types != [''] else None,
block_keys=block_keys if block_keys != [''] else None,
date_types=date_types if date_types != [''] else None
)
all_user_dates.update({str(key[0]): value for key, value in user_dates.items()})
return Response(all_user_dates)
10 changes: 2 additions & 8 deletions edx_when/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@
URLs for edx_when.
"""

from django.conf import settings
from django.urls import re_path
from django.urls import include, path

from . import views

app_name = 'edx_when'

urlpatterns = [
re_path(
r'edx_when/course/{}'.format(settings.COURSE_ID_PATTERN),
views.CourseDates.as_view(),
name='course_dates'
)
path('edx_when/v1/', include('edx_when.rest_api.v1.urls'), name='v1'),
]
17 changes: 0 additions & 17 deletions edx_when/views.py

This file was deleted.

Loading