From 722a1074fba65edbe45823d71513f53a31a14b14 Mon Sep 17 00:00:00 2001 From: andrii-hantkovskyi <131773947+andrii-hantkovskyi@users.noreply.github.com> Date: Tue, 7 May 2024 13:21:49 +0300 Subject: [PATCH] refactor: [ACI-956] little code cleanup (#163) * refactor: [ACI-956] little code cleanup - remove unused imports, variables - add missing docstrings - add & fix tests * fix: [ACI-963] remove migration conflict --------- Co-authored-by: Andrii --- credentials/apps/badges/admin.py | 19 +++ credentials/apps/badges/admin_forms.py | 19 +++ credentials/apps/badges/models.py | 31 +++- .../apps/badges/tests/test_admin_forms.py | 70 ++++++++ .../apps/badges/tests/test_services.py | 158 +++++++++--------- credentials/apps/badges/tests/test_utils.py | 88 ++++++++-- credentials/apps/badges/utils.py | 16 +- credentials/settings/test.py | 5 - 8 files changed, 301 insertions(+), 105 deletions(-) create mode 100644 credentials/apps/badges/tests/test_admin_forms.py diff --git a/credentials/apps/badges/admin.py b/credentials/apps/badges/admin.py index 831e495f5..2bf6f012b 100644 --- a/credentials/apps/badges/admin.py +++ b/credentials/apps/badges/admin.py @@ -33,6 +33,10 @@ class BadgeRequirementInline(admin.TabularInline): + """ + Badge template requirement inline setup. + """ + model = BadgeRequirement show_change_link = True extra = 0 @@ -60,12 +64,19 @@ def rules(self, obj): class BadgePenaltyInline(admin.TabularInline): + """ + Badge template penalty inline setup. + """ + model = BadgePenalty show_change_link = True extra = 0 form = BadgePenaltyForm def formfield_for_manytomany(self, db_field, request, **kwargs): + """ + Filter requirements by parent badge template. + """ if db_field.name == "requirements": template_id = request.resolver_match.kwargs.get("object_id") if template_id: @@ -74,6 +85,10 @@ def formfield_for_manytomany(self, db_field, request, **kwargs): class FulfillmentInline(admin.TabularInline): + """ + Badge template fulfillment inline setup. + """ + model = Fulfillment extra = 0 readonly_fields = [ @@ -82,6 +97,10 @@ class FulfillmentInline(admin.TabularInline): class DataRuleInline(admin.TabularInline): + """ + Data rule inline setup. + """ + model = DataRule extra = 0 form = DataRuleForm diff --git a/credentials/apps/badges/admin_forms.py b/credentials/apps/badges/admin_forms.py index 6d3022bf7..1e02aff8b 100644 --- a/credentials/apps/badges/admin_forms.py +++ b/credentials/apps/badges/admin_forms.py @@ -65,11 +65,17 @@ def _ensure_organization_exists(self, api_client): class BadgePenaltyForm(forms.ModelForm): + """ + Form for BadgePenalty model. + """ class Meta: model = BadgePenalty fields = "__all__" def clean(self): + """ + Ensure that all penalties belong to the same template. + """ cleaned_data = super().clean() requirements = cleaned_data.get("requirements") @@ -81,13 +87,23 @@ def clean(self): class DataRuleFormSet(forms.BaseInlineFormSet): + """ + Formset for DataRule model. + """ def get_form_kwargs(self, index): + """ + Pass parent instance to the form. + """ + kwargs = super().get_form_kwargs(index) kwargs["parent_instance"] = self.instance return kwargs class DataRuleForm(forms.ModelForm): + """ + Form for DataRule model. + """ class Meta: model = DataRule fields = "__all__" @@ -95,6 +111,9 @@ class Meta: data_path = forms.ChoiceField() def __init__(self, *args, parent_instance=None, **kwargs): + """ + Load data paths based on the parent instance event type. + """ self.parent_instance = parent_instance super().__init__(*args, **kwargs) diff --git a/credentials/apps/badges/models.py b/credentials/apps/badges/models.py index 4f4da940d..c488c8292 100644 --- a/credentials/apps/badges/models.py +++ b/credentials/apps/badges/models.py @@ -219,6 +219,10 @@ def reset(self, username: str): return bool(deleted) def is_fulfilled(self, username: str) -> bool: + """ + Checks if the requirement is fulfilled for the user. + """ + return self.fulfillments.filter(progress__username=username, progress__template=self.template).exists() def apply_rules(self, data: dict) -> bool: @@ -291,6 +295,7 @@ def apply(self, data: dict) -> bool: and `self.value` is "30", then calling `apply({"user": {"age": 30}})` will return True because the age matches the specified value. """ + comparison_func = getattr(operator, self.operator, None) if comparison_func: data_value = str(keypath(data, self.data_path)) @@ -357,9 +362,14 @@ def apply_rules(self, data: dict) -> bool: """ Evaluates payload rules. """ + return all(rule.apply(data) for rule in self.rules.all()) def reset_requirements(self, username: str): + """ + Resets all related requirements for the user. + """ + for requirement in self.requirements.all(): requirement.reset(username) @@ -419,6 +429,7 @@ def for_user(cls, *, username, template_id): """ Service shortcut. """ + progress, __ = cls.objects.get_or_create(username=username, template_id=template_id) return progress @@ -426,16 +437,12 @@ def for_user(cls, *, username, template_id): def ratio(self) -> float: """ Calculates badge template progress ratio. - - FIXME: simplify """ requirements = BadgeRequirement.objects.filter(template=self.template) - group_ids = requirements.filter(group__isnull=False).values_list("group", flat=True).distinct() requirements_count = requirements.filter(group__isnull=True).count() + group_ids.count() - fulfilled_requirements_count = Fulfillment.objects.filter( progress=self, requirement__template=self.template, @@ -444,15 +451,24 @@ def ratio(self) -> float: for group_id in group_ids: group_requirements = requirements.filter(group=group_id) - group_fulfillment_count = Fulfillment.objects.filter(requirement__in=group_requirements).count() - fulfilled_requirements_count += 1 if group_fulfillment_count > 0 else 0 + group_fulfilled_requirements_count = Fulfillment.objects.filter( + progress=self, + requirement__in=group_requirements, + ).count() + + if group_fulfilled_requirements_count > 0: + fulfilled_requirements_count += 1 - if fulfilled_requirements_count == 0: + if fulfilled_requirements_count == 0 or requirements_count == 0: return 0.00 return round(fulfilled_requirements_count / requirements_count, 2) @property def completed(self): + """ + Checks if the badge template is completed. + """ + return self.ratio == 1.00 def validate(self): @@ -556,4 +572,5 @@ def propagated(self): """ Checks if this user credential already has issued (external) Credly badge. """ + return self.external_uuid and (self.state in self.ISSUING_STATES) diff --git a/credentials/apps/badges/tests/test_admin_forms.py b/credentials/apps/badges/tests/test_admin_forms.py new file mode 100644 index 000000000..85bb707a9 --- /dev/null +++ b/credentials/apps/badges/tests/test_admin_forms.py @@ -0,0 +1,70 @@ +import uuid + +from django import forms +from django.contrib.sites.models import Site +from django.test import TestCase +from django.utils.translation import gettext as _ + +from credentials.apps.badges.admin_forms import BadgePenaltyForm +from credentials.apps.badges.models import BadgeRequirement, BadgeTemplate + + +COURSE_PASSING_EVENT = "org.openedx.learning.course.passing.status.updated.v1" + + +class BadgePenaltyFormTestCase(TestCase): + def setUp(self): + self.site = Site.objects.create(domain="test_domain", name="test_name") + self.badge_template1 = BadgeTemplate.objects.create( + uuid=uuid.uuid4(), name="test_template", state="draft", site=self.site + ) + self.badge_template2 = BadgeTemplate.objects.create( + uuid=uuid.uuid4(), name="test_template", state="draft", site=self.site + ) + self.requirement1 = BadgeRequirement.objects.create( + template=self.badge_template1, + event_type=COURSE_PASSING_EVENT, + description="Test course passing award description 1", + ) + self.requirement2 = BadgeRequirement.objects.create( + template=self.badge_template2, + event_type=COURSE_PASSING_EVENT, + description="Test course passing award description 2", + ) + self.requirement3 = BadgeRequirement.objects.create( + template=self.badge_template2, + event_type=COURSE_PASSING_EVENT, + description="Test course passing award description 3", + ) + + def test_clean_requirements_same_template(self): + form = BadgePenaltyForm() + form.cleaned_data = { + "template": self.badge_template2, + "requirements": [ + self.requirement2, + self.requirement3, + ], + } + self.assertEqual(form.clean(), { + "template": self.badge_template2, + "requirements": [ + self.requirement2, + self.requirement3, + ], + }) + + def test_clean_requirements_different_template(self): + form = BadgePenaltyForm() + form.cleaned_data = { + "template": self.badge_template1, + "requirements": [ + self.requirement2, + self.requirement1, + ], + } + + with self.assertRaises(forms.ValidationError) as cm: + form.clean() + + self.assertEqual(str(cm.exception), "['All requirements must belong to the same template.']") \ No newline at end of file diff --git a/credentials/apps/badges/tests/test_services.py b/credentials/apps/badges/tests/test_services.py index 3a05edc53..fb124a8fb 100644 --- a/credentials/apps/badges/tests/test_services.py +++ b/credentials/apps/badges/tests/test_services.py @@ -2,19 +2,37 @@ from django.contrib.sites.models import Site from django.test import TestCase +from opaque_keys.edx.keys import CourseKey +from openedx_events.learning.data import CourseData, CoursePassingStatusData, UserData, UserPersonalData from credentials.apps.badges.models import ( BadgePenalty, BadgeProgress, BadgeRequirement, BadgeTemplate, + CredlyBadgeTemplate, CredlyOrganization, DataRule, Fulfillment, PenaltyDataRule, ) +from credentials.apps.badges.processing.generic import identify_user from credentials.apps.badges.processing.progression import discover_requirements, process_requirements from credentials.apps.badges.processing.regression import discover_penalties, process_penalties +from credentials.apps.badges.signals import BADGE_PROGRESS_COMPLETE +from credentials.apps.badges.signals.handlers import handle_badge_completion + + +COURSE_PASSING_EVENT = "org.openedx.learning.course.passing.status.updated.v1" +COURSE_PASSING_DATA = CoursePassingStatusData( + status="passing", + course=CourseData(course_key=CourseKey.from_string("course-v1:edX+DemoX.1+2014"), display_name="A"), + user=UserData( + id=1, + is_active=True, + pii=UserPersonalData(username="test_username", email="test_email", name="John Doe"), + ), +) class BadgeRequirementDiscoveryTestCase(TestCase): @@ -26,13 +44,13 @@ def setUp(self): self.badge_template = BadgeTemplate.objects.create( uuid=uuid.uuid4(), name="test_template", state="draft", site=self.site, is_active=True ) - self.COURSE_PASSING_EVENT = "org.openedx.learning.course.passing.status.updated.v1" + self.CCX_COURSE_PASSING_EVENT = "org.openedx.learning.ccx.course.passing.status.updated.v1" def test_discovery_eventtype_related_requirements(self): BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="Test course passing award description", ) BadgeRequirement.objects.create( @@ -45,7 +63,7 @@ def test_discovery_eventtype_related_requirements(self): event_type=self.CCX_COURSE_PASSING_EVENT, description="Test ccx course passing revoke description", ) - course_passing_requirements = discover_requirements(event_type=self.COURSE_PASSING_EVENT) + course_passing_requirements = discover_requirements(event_type=COURSE_PASSING_EVENT) ccx_course_passing_requirements = discover_requirements(event_type=self.CCX_COURSE_PASSING_EVENT) self.assertEqual(course_passing_requirements.count(), 1) self.assertEqual(ccx_course_passing_requirements.count(), 2) @@ -63,15 +81,15 @@ def setUp(self): self.badge_template = BadgeTemplate.objects.create( uuid=uuid.uuid4(), name="test_template", state="draft", site=self.site, is_active=True ) - self.COURSE_PASSING_EVENT = "org.openedx.learning.course.passing.status.updated.v1" + self.CCX_COURSE_PASSING_EVENT = "org.openedx.learning.ccx.course.passing.status.updated.v1" def test_discovery_eventtype_related_penalties(self): - penalty1 = BadgePenalty.objects.create(template=self.badge_template, event_type=self.COURSE_PASSING_EVENT) + penalty1 = BadgePenalty.objects.create(template=self.badge_template, event_type=COURSE_PASSING_EVENT) penalty1.requirements.add( BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="Test course passing award description", ) ) @@ -91,7 +109,7 @@ def test_discovery_eventtype_related_penalties(self): description="Test ccx course passing revoke description", ) ) - course_passing_penalties = discover_penalties(event_type=self.COURSE_PASSING_EVENT) + course_passing_penalties = discover_penalties(event_type=COURSE_PASSING_EVENT) ccx_course_passing_penalties = discover_penalties(event_type=self.CCX_COURSE_PASSING_EVENT) self.assertEqual(course_passing_penalties.count(), 1) self.assertEqual(ccx_course_passing_penalties.count(), 2) @@ -117,18 +135,18 @@ def setUp(self): self.badge_template = BadgeTemplate.objects.create( uuid=uuid.uuid4(), name="test_template", state="draft", site=self.site ) - self.COURSE_PASSING_EVENT = "org.openedx.learning.course.passing.status.updated.v1" + self.CCX_COURSE_PASSING_EVENT = "org.openedx.learning.ccx.course.passing.status.updated.v1" def test_process_penalties_all_datarules_success(self): requirement1 = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="Test course passing award description 1", ) requirement2 = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="Test course passing award description 2", ) DataRule.objects.create( @@ -153,43 +171,30 @@ def test_process_penalties_all_datarules_success(self): self.assertEqual(Fulfillment.objects.filter(progress=progress, requirement=requirement1).count(), 1) self.assertEqual(Fulfillment.objects.filter(progress=progress, requirement=requirement1).count(), 1) - bp = BadgePenalty.objects.create(template=self.badge_template, event_type=self.COURSE_PASSING_EVENT) + bp = BadgePenalty.objects.create(template=self.badge_template, event_type=COURSE_PASSING_EVENT) bp.requirements.set( (requirement1, requirement2), ) PenaltyDataRule.objects.create( penalty=bp, - data_path="course_passing_status.user.pii.username", + data_path="course.display_name", operator="ne", value="test_username1", ) - PenaltyDataRule.objects.create( - penalty=bp, - data_path="course_passing_status.user.pii.email", - operator="ne", - value="test_email1", - ) self.badge_template.is_active = True self.badge_template.save() - kwargs = { - "course_passing_status": { - "user": { - "pii": {"username": "test_username", "email": "test_email", "name": "test_name"}, - } - } - } - process_penalties(self.COURSE_PASSING_EVENT, "test_username", kwargs) + process_penalties(COURSE_PASSING_EVENT, "test_username", COURSE_PASSING_DATA) self.assertEqual(Fulfillment.objects.filter(progress=progress).count(), 0) def test_process_penalties_one_datarule_fail(self): requirement1 = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="Test course passing award description 1", ) requirement2 = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="Test course passing award description 2", ) DataRule.objects.create( @@ -215,30 +220,23 @@ def test_process_penalties_one_datarule_fail(self): self.assertEqual(Fulfillment.objects.filter(progress=progress, requirement=requirement1).count(), 1) BadgePenalty.objects.create( - template=self.badge_template, event_type=self.COURSE_PASSING_EVENT + template=self.badge_template, event_type=COURSE_PASSING_EVENT ).requirements.set( (requirement1, requirement2), ) PenaltyDataRule.objects.create( penalty=BadgePenalty.objects.first(), - data_path="course_passing_status.user.pii.username", - operator="ne", - value="test_username", + data_path="course.display_name", + operator="eq", + value="A", ) PenaltyDataRule.objects.create( penalty=BadgePenalty.objects.first(), - data_path="course_passing_status.user.pii.email", + data_path="course.display_name", operator="ne", - value="test_email", + value="A", ) - kwargs = { - "course_passing_status": { - "user": { - "pii": {"username": "test_username", "email": "test_email", "name": "test_name"}, - } - } - } - process_penalties(self.COURSE_PASSING_EVENT, "test_username", kwargs) + process_penalties(COURSE_PASSING_EVENT, "test_username", COURSE_PASSING_DATA) self.assertEqual(Fulfillment.objects.filter(progress=progress).count(), 2) @@ -248,15 +246,20 @@ def setUp(self): uuid=uuid.uuid4(), api_key="test-api-key", name="test_organization" ) self.site = Site.objects.create(domain="test_domain", name="test_name") - self.badge_template = BadgeTemplate.objects.create( + self.badge_template = CredlyBadgeTemplate.objects.create( uuid=uuid.uuid4(), name="test_template", state="draft", site=self.site, + organization=self.organization, is_active=True, ) - self.COURSE_PASSING_EVENT = "org.openedx.learning.course.passing.status.updated.v1" + self.CCX_COURSE_PASSING_EVENT = "org.openedx.learning.ccx.course.passing.status.updated.v1" + self.user = identify_user(event_type=COURSE_PASSING_EVENT, event_payload=COURSE_PASSING_DATA) + + # disconnect BADGE_PROGRESS_COMPLETE signal + BADGE_PROGRESS_COMPLETE.disconnect(handle_badge_completion) # test cases # A course completion - course A w/o a group; @@ -269,7 +272,7 @@ def setUp(self): def test_course_a_completion(self): requirement = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A course passing award description", ) DataRule.objects.create( @@ -278,22 +281,19 @@ def test_course_a_completion(self): operator="eq", value="A", ) - kwargs = { - "course": {"display_name": "A"}, - } - process_requirements(self.COURSE_PASSING_EVENT, "test_username", kwargs) + process_requirements(COURSE_PASSING_EVENT, "test_username", COURSE_PASSING_DATA) self.assertEqual(Fulfillment.objects.filter(requirement=requirement).count(), 1) def test_course_a_or_b_completion(self): requirement_a = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B course passing award description", group="a_or_b", ) requirement_b = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B course passing award description", group="a_or_b", ) @@ -309,10 +309,7 @@ def test_course_a_or_b_completion(self): operator="eq", value="B", ) - kwargs = { - "course": {"display_name": "A"}, - } - process_requirements(self.COURSE_PASSING_EVENT, "test_username", kwargs) + 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) @@ -320,19 +317,19 @@ def test_course_a_or_b_completion(self): def test_course_a_or_b_or_c_completion(self): requirement_a = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B or C course passing award description", group="a_or_b_or_c", ) requirement_b = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B or C course passing award description", group="a_or_b_or_c", ) requirement_c = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B or C course passing award description", group="a_or_b_or_c", ) @@ -354,10 +351,7 @@ def test_course_a_or_b_or_c_completion(self): operator="eq", value="C", ) - kwargs = { - "course": {"display_name": "A"}, - } - process_requirements(self.COURSE_PASSING_EVENT, "test_username", kwargs) + 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) @@ -366,7 +360,7 @@ def test_course_a_or_b_or_c_completion(self): def test_course_a_or_completion(self): requirement = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or course passing award description", group="a_or", ) @@ -376,29 +370,26 @@ def test_course_a_or_completion(self): operator="eq", value="A", ) - kwargs = { - "course": {"display_name": "A"}, - } - process_requirements(self.COURSE_PASSING_EVENT, "test_username", kwargs) + 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) def test_course_a_or_b_and_c_completion(self): requirement_a = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B course passing award description", group="a_or_b", ) requirement_b = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B course passing award description", group="a_or_b", ) requirement_c = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="C course passing award description", ) DataRule.objects.create( @@ -419,10 +410,7 @@ def test_course_a_or_b_and_c_completion(self): operator="eq", value="A", ) - kwargs = { - "course": {"display_name": "A"}, - } - process_requirements(self.COURSE_PASSING_EVENT, "test_username", kwargs) + 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(), 1) @@ -431,25 +419,25 @@ def test_course_a_or_b_and_c_completion(self): def test_course_a_or_b_and_c_or_d_completion(self): requirement_a = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B course passing award description", group="a_or_b", ) requirement_b = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="A or B course passing award description", group="a_or_b", ) requirement_c = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="C or D course passing award description", group="c_or_d", ) requirement_d = BadgeRequirement.objects.create( template=self.badge_template, - event_type=self.COURSE_PASSING_EVENT, + event_type=COURSE_PASSING_EVENT, description="C or D course passing award description", group="c_or_d", ) @@ -477,12 +465,18 @@ def test_course_a_or_b_and_c_or_d_completion(self): operator="eq", value="D", ) - kwargs = { - "course": {"display_name": "A"}, - } - process_requirements(self.COURSE_PASSING_EVENT, "test_username", kwargs) + 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(), 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) + + def tearDown(self): + BADGE_PROGRESS_COMPLETE.connect(handle_badge_completion) + + +class TestIdentifyUser(TestCase): + def test_identify_user(self): + username = identify_user(event_type=COURSE_PASSING_EVENT, event_payload=COURSE_PASSING_DATA) + self.assertEqual(username, "test_username") \ No newline at end of file diff --git a/credentials/apps/badges/tests/test_utils.py b/credentials/apps/badges/tests/test_utils.py index b6ae44182..3bb91f1a8 100644 --- a/credentials/apps/badges/tests/test_utils.py +++ b/credentials/apps/badges/tests/test_utils.py @@ -5,10 +5,12 @@ from django.conf import settings from unittest.mock import patch +from django.conf import settings from openedx_events.learning.data import UserData, UserPersonalData, CourseData, CoursePassingStatusData from credentials.apps.badges.checks import badges_checks -from credentials.apps.badges.utils import extract_payload, keypath, get_user_data, get_event_type_keypaths +from credentials.apps.badges.credly.utils import get_credly_base_url, get_credly_api_base_url +from credentials.apps.badges.utils import credly_check, extract_payload, get_event_type_keypaths, get_user_data, keypath class TestKeypath(unittest.TestCase): @@ -127,15 +129,81 @@ def test_badges_checks_non_empty_events(self, mock_get_badging_event_types): self.assertEqual(len(errors), 0) -class TestGetEventTypeKeypaths(unittest.TestCase): - def setUp(self): - self.EVENT_TYPE = "org.openedx.learning.course.passing.status.updated.v1" +class TestCredlyCheck(unittest.TestCase): + def test_credly_configured(self): + settings.BADGES_CONFIG = { + "credly": { + "CREDLY_BASE_URL": "https://www.credly.com", + "CREDLY_API_BASE_URL": "https://api.credly.com", + "CREDLY_SANDBOX_BASE_URL": "https://sandbox.credly.com", + "CREDLY_SANDBOX_API_BASE_URL": "https://sandbox.api.credly.com", + "USE_SANDBOX": True, + } + } + result = credly_check() + self.assertTrue(result) + + def test_credly_not_configured(self): + settings.BADGES_CONFIG = {} + result = credly_check() + self.assertFalse(result) + + def test_credly_missing_keys(self): + settings.BADGES_CONFIG = { + "credly": { + "CREDLY_BASE_URL": "https://www.credly.com", + "CREDLY_API_BASE_URL": "https://api.credly.com", + "USE_SANDBOX": True, + } + } + result = credly_check() + self.assertFalse(result) + +class TestGetEventTypeKeypaths(unittest.TestCase): def test_get_event_type_keypaths(self): - event_type_keypaths = get_event_type_keypaths(self.EVENT_TYPE) - self.assertIsNotNone(event_type_keypaths) - self.assertIn("status", event_type_keypaths) - self.assertIn("course.display_name", event_type_keypaths) + result = get_event_type_keypaths("org.openedx.learning.course.passing.status.updated.v1") + expected_keypaths = ["status", "course.display_name", "course.start", "course.end"] + for exp_keypath in expected_keypaths: + self.assertIn(exp_keypath, result) + + +class TestGetCredlyBaseUrl(unittest.TestCase): + def test_get_credly_base_url_sandbox(self): + settings.BADGES_CONFIG["credly"] = { + "CREDLY_BASE_URL": "https://www.credly.com", + "CREDLY_SANDBOX_BASE_URL": "https://sandbox.credly.com", + "USE_SANDBOX": True, + } + result = get_credly_base_url(settings) + self.assertEqual(result, "https://sandbox.credly.com") + + def test_get_credly_base_url_production(self): + settings.BADGES_CONFIG["credly"] = { + "CREDLY_BASE_URL": "https://www.credly.com", + "CREDLY_SANDBOX_BASE_URL": "https://sandbox.credly.com", + "USE_SANDBOX": False, + } + result = get_credly_base_url(settings) + self.assertEqual(result, "https://www.credly.com") - for excluded_keypath in settings.BADGES_CONFIG.get("EXCLUDED_KEY_PATHS", []): - self.assertNotIn(excluded_keypath, event_type_keypaths) + +class TestGetCredlyApiBaseUrl(unittest.TestCase): + def test_get_credly_api_base_url_sandbox(self): + settings.BADGES_CONFIG["credly"] = { + "CREDLY_API_BASE_URL": "https://api.credly.com", + "CREDLY_SANDBOX_API_BASE_URL": "https://sandbox.api.credly.com", + "USE_SANDBOX": True, + } + + result = get_credly_api_base_url(settings) + self.assertEqual(result, "https://sandbox.api.credly.com") + + def test_get_credly_api_base_url_production(self): + settings.BADGES_CONFIG["credly"] = { + "CREDLY_API_BASE_URL": "https://api.credly.com", + "CREDLY_SANDBOX_API_BASE_URL": "https://sandbox.api.credly.com", + "USE_SANDBOX": False, + } + result = get_credly_api_base_url(settings) + self.assertEqual(result, "https://api.credly.com") diff --git a/credentials/apps/badges/utils.py b/credentials/apps/badges/utils.py index 1a5b9e352..aae4596ca 100644 --- a/credentials/apps/badges/utils.py +++ b/credentials/apps/badges/utils.py @@ -3,7 +3,7 @@ from attrs import asdict from django.conf import settings -from openedx_events.learning.data import UserData, UserPersonalData +from openedx_events.learning.data import UserData from openedx_events.tooling import OpenEdxPublicSignal @@ -15,6 +15,10 @@ def get_badging_event_types(): def credly_check(): + """ + Checks if Credly is configured. + """ + credly_settings = settings.BADGES_CONFIG.get("credly", None) if credly_settings is None: return False @@ -50,10 +54,15 @@ def keypath(payload, keys_path): >>> keypath(payload, 'a.b.d') None """ + keys = keys_path.split(".") current = payload def traverse(current, keys): + """ + Recursive function to traverse the dictionary. + """ + if not keys: return current key = keys[0] @@ -116,10 +125,15 @@ def get_event_type_keypaths(event_type: str) -> list: Returns: list: A list of all possible keypaths for the given event type. """ + signal = OpenEdxPublicSignal.get_signal_by_type(event_type) data = extract_payload(signal.init_data) def get_data_keypaths(data): + """ + Extracts all possible keypaths for a given dataclass. + """ + keypaths = [] for field in attr.fields(data): if attr.has(field.type): diff --git a/credentials/settings/test.py b/credentials/settings/test.py index 8f57a4772..b0c84dc8f 100644 --- a/credentials/settings/test.py +++ b/credentials/settings/test.py @@ -51,9 +51,4 @@ STATICFILES_STORAGE = None add_plugins(__name__, PROJECT_TYPE, SettingsType.TEST) -BADGES_ENABLED = True -BADGES_CONFIG["events"] = [ - "org.openedx.learning.course.passing.status.updated.v1", - "org.openedx.learning.ccx.course.passing.status.updated.v1", -] BADGES_CONFIG["credly"]["USE_SANDBOX"] = True