Skip to content

Commit

Permalink
feat: adding pathway status (#2710)
Browse files Browse the repository at this point in the history
* feat: adding pathway status

course-discovery is adding the concept of retired  pathways, which it
will expose with a status field through the API. We still want to ingest
retired and unpublished pathways, because we don't want to break any of
our existing foreign key relationships. However, we don't want to expose
them to either the Learner Record MFE  or the program dashboard.


FIXES: APER-3929
  • Loading branch information
deborahgu authored Feb 27, 2025
1 parent c484bdb commit d42973e
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 9 deletions.
15 changes: 12 additions & 3 deletions credentials/apps/catalog/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,18 @@ class ProgramAdmin(admin.ModelAdmin):

@admin.register(Pathway)
class PathwayAdmin(admin.ModelAdmin):
list_display = ("name", "org_name", "pathway_type", "email", "uuid")
list_filter = ("site",)
readonly_fields = ("name", "org_name", "pathway_type", "email", "uuid", "site", "programs")
list_display = ("name", "org_name", "pathway_type", "status", "email", "uuid")
list_filter = ("site", "status")
readonly_fields = (
"name",
"org_name",
"pathway_type",
"email",
"uuid",
"site",
"programs",
"status",
)
search_fields = ("name", "uuid")


Expand Down
6 changes: 6 additions & 0 deletions credentials/apps/catalog/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,9 @@ class ProgramStatus(Enum):
RETIRED = "retired"
DELETED = "deleted"
UNPUBLISHED = "unpublished"


class PathwayStatus(Enum):
RETIRED = "retired"
UNPUBLISHED = "unpublished"
PUBLISHED = "published"
14 changes: 14 additions & 0 deletions credentials/apps/catalog/docs/decisions/0002-pathway-status.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Pathway Status
==============

Status
------
Accepted

Background
----------
Course discovery is now allowing pathways to be retired. The `course-discovery` API will continue to expose retired pathways, and it will rely on consumers to expose or process retired pathways as appropriate.

Decision
--------
The catalog app will now synchronize the `Pathway`'s `status` attribute. In the absence of a populated `status` attribute, a pathway will be considered to have the status of `published`.
18 changes: 18 additions & 0 deletions credentials/apps/catalog/migrations/0016_pathway_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.19 on 2025-02-25 20:14

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("catalog", "0015_auto_20201130_2041"),
]

operations = [
migrations.AddField(
model_name="pathway",
name="status",
field=models.CharField(max_length=24, null=True),
),
]
4 changes: 4 additions & 0 deletions credentials/apps/catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ class Pathway(TimeStampedModel):
email = models.EmailField()
programs = SortedManyToManyField(Program, related_name="pathways")

# We're not migration-creating a status of all the old Pathways,
# we're just treating them as inherently 'published'.
status = models.CharField(max_length=24, null=True, blank=False)

class Meta:
unique_together = (("site", "uuid"),)

Expand Down
2 changes: 2 additions & 0 deletions credentials/apps/catalog/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pytz import UTC
from slugify import slugify

from credentials.apps.catalog.data import PathwayStatus
from credentials.apps.catalog.models import Course, CourseRun, Organization, Pathway, Program
from credentials.apps.core.tests.factories import SiteFactory

Expand Down Expand Up @@ -90,6 +91,7 @@ class Meta:
name = FuzzyText(prefix="Test Pathway ")
org_name = FuzzyText()
email = factory.Faker("safe_email")
status = PathwayStatus.PUBLISHED.value

@factory.post_generation
def programs(self, create, extracted):
Expand Down
3 changes: 3 additions & 0 deletions credentials/apps/catalog/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class SynchronizerTests(TestCase):
"name": "First Pathway",
"org_name": "First Org Name",
"email": "test@example.com",
"status": "published",
"programs": [FIRST_PROGRAM],
"pathway_type": PathwayType.INDUSTRY.value,
} # Check type is industry since type defaults to credit
Expand Down Expand Up @@ -150,6 +151,7 @@ def test_fetch_data_create(self):
# Check pathway
pathway = Pathway.objects.all().first()
assert str(pathway.uuid) == self.FIRST_PATHWAY["uuid"]
assert pathway.status == self.FIRST_PATHWAY["status"]
assert list(pathway.programs.all()) == [program]

def test_fetch_data_update(self):
Expand Down Expand Up @@ -202,6 +204,7 @@ def test_fetch_data_update(self):
# Check pathway still exists and has updated program
pathway = Pathway.objects.all().first()
assert str(pathway.uuid) == self.FIRST_PATHWAY["uuid"]
assert pathway.status == self.FIRST_PATHWAY["status"]
assert list(pathway.programs.all()) == [program]

def test_remove_obsolete_data(self):
Expand Down
15 changes: 10 additions & 5 deletions credentials/apps/catalog/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from django.db import transaction

from credentials.apps.catalog.data import PathwayStatus
from credentials.apps.catalog.models import Course, CourseRun, Organization, Pathway, Program


Expand Down Expand Up @@ -315,9 +316,11 @@ def _parse_course_run(self, course, data):
@transaction.atomic
def _parse_pathway(self, data):
"""
Creates or updates a pathway and links it to connected programs
Creates or updates a pathway and links it to connected programs.
Assumes that the associated programs were parsed before this is run.
* Assumes that the associated programs were parsed before this is run.
* Always re-creates the foreign keys between Pathway and Program on modification.
If the Pathway is retired or unpublished, no relationship is created
Arguments:
data (dict): The pathway data pulled from the API
Expand All @@ -332,15 +335,17 @@ def _parse_pathway(self, data):
"name": data["name"],
"email": data["email"],
"org_name": data["org_name"],
"status": data["status"],
"pathway_type": data["pathway_type"],
},
)

self.add_item(self.PATHWAY, str(pathway.uuid))

pathway.programs.clear()
for program_data in data["programs"]:
program = Program.objects.get(site=self.site, uuid=program_data["uuid"])
pathway.programs.add(program)
if pathway.status in ("", PathwayStatus.PUBLISHED.value):
for program_data in data["programs"]:
program = Program.objects.get(site=self.site, uuid=program_data["uuid"])
pathway.programs.add(program)

return pathway
7 changes: 6 additions & 1 deletion credentials/apps/records/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ def _get_transformed_pathway_data(program, user):
program_pathways_set = frozenset(program_pathways)

user_credit_pathways = (
UserCreditPathway.objects.select_related("pathway").filter(user=user, pathway__in=program_pathways_set).all()
UserCreditPathway.objects.select_related("pathway")
.filter(
user=user,
pathway__in=program_pathways_set,
)
.all()
)
# maps a learner's pathway status to a pathway
user_credit_pathways_dict = {user_pathway.pathway: user_pathway.status for user_pathway in user_credit_pathways}
Expand Down
16 changes: 16 additions & 0 deletions credentials/apps/records/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from django.test.utils import override_settings
from django.urls import reverse

from credentials.apps.catalog.data import PathwayStatus
from credentials.apps.catalog.tests.factories import (
CourseFactory,
CourseRunFactory,
Expand Down Expand Up @@ -105,6 +106,7 @@ def test_pcr_already_exists(self):
self.assertEqual(url1, url2)


@ddt.ddt
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
class ProgramSendTests(SiteMixin, TestCase):
USERNAME = "test-user"
Expand Down Expand Up @@ -208,6 +210,20 @@ def test_email_content_complete(self):
self.assertEqual(self.user.email, email.reply_to[0])
self.assertListEqual([self.pathway.email], email.to)

@ddt.data(
(PathwayStatus.PUBLISHED.value, 200),
(PathwayStatus.UNPUBLISHED.value, 404),
(PathwayStatus.RETIRED.value, 404),
("", 200),
)
@ddt.unpack
def test_pathway_must_be_published(self, pathway_status, http_status):
"""Verify a pathway only sends if its status is published or empty"""
self.pathway.status = pathway_status
self.pathway.save()
response = self.post()
self.assertEqual(response.status_code, http_status)

def test_email_content_incomplete(self):
"""Verify an email is actually sent"""
self.user_credential.delete()
Expand Down
2 changes: 2 additions & 0 deletions credentials/apps/records/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from edx_ace import Recipient, ace
from segment.analytics.client import Client as SegmentClient

from credentials.apps.catalog.data import PathwayStatus
from credentials.apps.catalog.models import Pathway, Program
from credentials.apps.core.api import get_user_by_username
from credentials.apps.core.views import ThemeViewMixin
Expand Down Expand Up @@ -150,6 +151,7 @@ def post(self, request, **kwargs):
Pathway,
id=pathway_id,
programs__uuid=program_uuid,
status__in=(PathwayStatus.PUBLISHED.value, ""),
pathway_type=PathwayType.CREDIT.value,
)
certificate = get_object_or_404(ProgramCertificate, program_uuid=program_uuid, site=request.site)
Expand Down

0 comments on commit d42973e

Please sign in to comment.