Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions openedx/plugins/subscriber/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include README.rst
include openedx.yaml
recursive-include subscriber *.py
5 changes: 5 additions & 0 deletions openedx/plugins/subscriber/openedx.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lms:
django_apps:
- subscriber
urls:
- subscriber.urls
16 changes: 16 additions & 0 deletions openedx/plugins/subscriber/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from setuptools import setup, find_packages

setup(
name="platform-plugin-subscriber",
version="0.1.0",
packages=find_packages(),
include_package_data=True,
package_data={
"": ["openedx.yaml"],
},
entry_points={
"lms.djangoapp": [
"subscriber = subscriber.apps:SubscriberConfig",
],
},
)
1 change: 1 addition & 0 deletions openedx/plugins/subscriber/subscriber/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = "subscriber.apps.SubscriberConfig"
15 changes: 15 additions & 0 deletions openedx/plugins/subscriber/subscriber/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.apps import AppConfig


class SubscriberConfig(AppConfig):
name = "subscriber"

plugin_app = {
"url_config": {
"lms.djangoapp": {
"namespace": "subscriber",
"regex": "^api/subscriber/",
"relative_path": "urls",
}
}
}
42 changes: 42 additions & 0 deletions openedx/plugins/subscriber/subscriber/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from common.djangoapps.student.models import CourseEnrollment

# POC-only: hardcoded subscription catalog
SUBSCRIPTION_COURSE_IDS = [
"course-v1:edX+DemoX+Demo_Course",
]


def get_categorized_courses(user):
"""
Fetch user's enrolled courses and categorize them
for the Subscriber Learner Dashboard.
"""

enrollments = CourseEnrollment.objects.filter(
user=user,
is_active=True
)

subscription_courses = []
upgradeable_courses = []
non_upgradeable_courses = []

# POC assumption: user is already a subscriber
user_is_subscriber = True

for enrollment in enrollments:
course_id = str(enrollment.course_id)

if course_id in SUBSCRIPTION_COURSE_IDS:
if user_is_subscriber:
subscription_courses.append(course_id)
else:
upgradeable_courses.append(course_id)
else:
non_upgradeable_courses.append(course_id)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part that we should clarify.

Assume that SUBSCRIPTION_COURSE_IDS will eventually be an API call that we make to the Subscription catalog that another team will create.

From that payload, we'll need to check for each course enrollment:

  1. Is the course part of the Subscription catalog? If yes:
    a. Did the user fulfill either enrolling in the course after becoming a Subscriber or upgrading their audited course into their Subscription? (This is likely to mean that in the Enrollments payload we will know if the course is upgraded to a full-access.)
    • If yes to either, then this course falls under subscription_courses.
    • If no to either, then this course falls under upgradeable_courses
  2. If no, then the course falls under non_upgradeable_courses.

There's an additional tricky part that I haven't confirmed with Kelly, which is what happens if prior to being a Subscriber the user fully upgraded a course. Where does that course live in these three categories? If the course technically is part of the Subscription catalog, does it go under subscription_courses? If it doesn't, does it go under non_upgradeable_courses even though it indeed is upgraded already? Maybe that 3rd category would be more accurately called non_subscription_courses. I don't know yet.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Jason, this is very helpful clarification.

For this POC, I used a hardcoded SUBSCRIPTION_COURSE_IDS list and a simulated user_is_subscriber flag to demonstrate that we can fetch enrollments and categorize courses via the platform plugin.

I agree that in the production implementation, SUBSCRIPTION_COURSE_IDS will be replaced with a Subscription Catalog API call, and categorization will depend on both:

• Whether the course exists in the Subscription catalog
• Whether the enrollment reflects full-access (subscriber enrollment or upgraded course)

Based on your guidance, the intended logic will be:

• If course is in Subscription catalog AND enrollment reflects subscriber/full-access → subscription_courses
• If course is in Subscription catalog BUT enrollment does not reflect subscriber/full-access → upgradeable_courses
• If course is not in Subscription catalog → non_upgradeable_courses

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s load the test data and run everything through integration tests first. This approach allows the final implementation to move forward without any guesswork and ensures we follow SOLID principles with a clear separation of responsibilities. Relying on hard‑coded values will restrict execution to only one specific flow, which limits flexibility. I understand this is a POC, but we can still put a clean skeleton in place now and expand it as the project evolves. We can also assume that the Swagger API specification will be provided later for us to integrate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback, @asharma4-sonata .
I’ve updated the implementation to remove hardcoded logic from the core flow and introduced a cleaner service structure with separate functions for fetching subscription catalog courses, checking subscriber status, and retrieving user enrollments. This prepares the plugin for integration testing with real LMS enrollment data and allows us to easily integrate with the future Subscription Catalog API once the Swagger specification is available. Please let me know if any additional adjustments are needed


return {
"subscription_courses": subscription_courses,
"upgradeable_courses": upgradeable_courses,
"non_upgradeable_courses": non_upgradeable_courses,
}
10 changes: 10 additions & 0 deletions openedx/plugins/subscriber/subscriber/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path
from .views import subscriber_courses

urlpatterns = [
path(
"dashboard/courses/",
subscriber_courses,
name="subscriber-dashboard-courses",
),
]
13 changes: 13 additions & 0 deletions openedx/plugins/subscriber/subscriber/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from .services import get_categorized_courses


@login_required
def subscriber_courses(request):
"""
API endpoint for Subscriber Learner Dashboard.
Returns user's enrolled courses grouped into categories.
"""
data = get_categorized_courses(request.user)
return JsonResponse(data)
Loading