Skip to content
Open
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
32 changes: 27 additions & 5 deletions credentials/apps/badges/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ def user_progress(self, username: str) -> float:
Determines a completion progress for user.
"""
progress = BadgeProgress.for_user(username=username, template_id=self.id)

if not progress:
return 0.00

return progress.ratio

def is_completed(self, username: str) -> bool:
Expand Down Expand Up @@ -215,7 +219,7 @@ def fulfill(self, username: str):
Returns: (bool) if progression happened
"""
template_id = self.template.id
progress = BadgeProgress.for_user(username=username, template_id=template_id)
progress = BadgeProgress.for_user(username=username, template_id=template_id, create_if_absent=True)
fulfillment, created = Fulfillment.objects.get_or_create(progress=progress, requirement=self, blend=self.blend)

if created:
Expand Down Expand Up @@ -275,6 +279,10 @@ def is_group_fulfilled(cls, *, group: str, template: BadgeTemplate, username: st
"""

progress = BadgeProgress.for_user(username=username, template_id=template.id)

if not progress:
return False

requirements = cls.objects.filter(template=template, blend=group)
fulfilled_requirements = requirements.filter(fulfillments__progress=progress).count()

Expand Down Expand Up @@ -509,13 +517,27 @@ def __str__(self):
return f"BadgeProgress:{self.username}"

@classmethod
def for_user(cls, *, username, template_id):
def for_user(cls, *, username, template_id, create_if_absent=False):
"""
Service shortcut.
Retrieve or create a BadgeProgress record for a user and template.

This method follows a lazy-load pattern to control when BadgeProgress records
are created. Use create_if_absent=False for read-only operations to avoid
creating orphaned records.

Args:
username: The username of the user to get or create progress for.
template_id: The ID of the BadgeTemplate to track progress for.
create_if_absent: Whether to create a new record if one doesn't exist.
- If True: Creates a new BadgeProgress record if needed
- If False: Returns None if the record doesn't exist, without creating
"""

progress, __ = cls.objects.get_or_create(username=username, template_id=template_id)
return progress
if create_if_absent:
progress, __ = cls.objects.get_or_create(username=username, template_id=template_id)
return progress

return cls.objects.filter(username=username, template_id=template_id).first()

@property
def ratio(self) -> float:
Expand Down
4 changes: 2 additions & 2 deletions credentials/apps/badges/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ def handle_requirement_fulfilled(sender, username, **kwargs):
"""
On user's Badge progression (completion).
"""
BadgeProgress.for_user(username=username, template_id=sender.template.id).progress()
BadgeProgress.for_user(username=username, template_id=sender.template.id, create_if_absent=True).progress()


@receiver(BADGE_REQUIREMENT_REGRESSED)
def handle_requirement_regressed(sender, username, **kwargs):
"""
On user's Badge regression (incompletion).
"""
BadgeProgress.for_user(username=username, template_id=sender.template.id).regress()
BadgeProgress.for_user(username=username, template_id=sender.template.id, create_if_absent=True).regress()


@receiver(BADGE_PROGRESS_COMPLETE)
Expand Down
66 changes: 55 additions & 11 deletions credentials/apps/badges/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,11 @@ def test_course_a_completion(self):
)
process_requirements(COURSE_PASSING_EVENT, "test_username", COURSE_PASSING_DATA)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement).count(), 1)
self.assertTrue(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertTrue(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

def test_course_a_or_b_completion(self):
"""
Expand Down Expand Up @@ -457,7 +461,11 @@ def test_course_a_or_b_completion(self):
process_requirements(COURSE_PASSING_EVENT, "test_username", COURSE_PASSING_DATA)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_a).count(), 1)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_b).count(), 0)
self.assertTrue(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertTrue(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

def test_course_a_or_b_or_c_completion(self):
"""
Expand Down Expand Up @@ -506,7 +514,11 @@ def test_course_a_or_b_or_c_completion(self):
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_a).count(), 1)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_b).count(), 0)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_c).count(), 0)
self.assertTrue(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertTrue(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

def test_course_a_or_completion(self):
"""
Expand All @@ -529,7 +541,11 @@ def test_course_a_or_completion(self):
)
process_requirements(COURSE_PASSING_EVENT, "test_username", COURSE_PASSING_DATA)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement).count(), 1)
self.assertTrue(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertTrue(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

def test_course_a_or_b_and_c_completion(self):
"""
Expand Down Expand Up @@ -572,7 +588,11 @@ def test_course_a_or_b_and_c_completion(self):
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_a).count(), 0)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_b).count(), 0)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_c).count(), 1)
self.assertFalse(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertFalse(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

DataRule.objects.create(
requirement=requirement_b,
Expand All @@ -587,7 +607,11 @@ def test_course_a_or_b_and_c_completion(self):
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_b).count(), 1)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_c).count(), 1)

self.assertTrue(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertTrue(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

def test_course_a_or_b_and_c_or_d_completion(self):
"""
Expand Down Expand Up @@ -639,15 +663,23 @@ def test_course_a_or_b_and_c_or_d_completion(self):
value="D",
)

self.assertFalse(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertFalse(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

process_requirements(COURSE_PASSING_EVENT, "test_username", COURSE_PASSING_DATA)

self.assertEqual(Fulfillment.objects.filter(requirement=requirement_a).count(), 1)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_b).count(), 0)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_c).count(), 0)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_d).count(), 0)
self.assertFalse(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertFalse(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

DataRule.objects.create(
requirement=requirement_c,
Expand All @@ -661,7 +693,11 @@ def test_course_a_or_b_and_c_or_d_completion(self):
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_b).count(), 0)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_c).count(), 1)
self.assertEqual(Fulfillment.objects.filter(requirement=requirement_d).count(), 0)
self.assertTrue(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertTrue(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)


class TestIdentifyUser(TestCase):
Expand Down Expand Up @@ -720,7 +756,11 @@ def setUp(self):
def test_process_event_passing(self):
event_payload = COURSE_PASSING_DATA
process_event(sender=self.sender, kwargs=event_payload)
self.assertTrue(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertTrue(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

def test_process_event_not_passing(self):
event_payload = CoursePassingStatusData(
Expand All @@ -733,7 +773,11 @@ def test_process_event_not_passing(self):
),
)
process_event(sender=self.sender, kwargs=event_payload)
self.assertFalse(BadgeProgress.for_user(username="test_username", template_id=self.badge_template.id).completed)
self.assertFalse(
BadgeProgress.for_user(
username="test_username", template_id=self.badge_template.id, create_if_absent=True
).completed
)

@patch.object(BadgeProgress, "regress", mock_progress_regress)
def test_process_event_not_found(self):
Expand Down