Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: define processing awarding flow steps #86

Merged
merged 1 commit into from
Mar 29, 2024
Merged
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
1 change: 0 additions & 1 deletion credentials/apps/badges/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.apps import AppConfig
from django.conf import settings

from .toggles import check_badges_enabled

Expand Down
63 changes: 0 additions & 63 deletions credentials/apps/badges/processing.py

This file was deleted.

6 changes: 2 additions & 4 deletions credentials/apps/badges/services/user_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
def create_user_credential(username, badge_template):
if not isinstance(username, str):
raise ValueError("`username` must be a string")

if not isinstance(badge_template, BadgeTemplate):
raise TypeError("`badge_template` must be an instance of BadgeTemplate")


UserCredential.objects.create(
credential_content_type=ContentType.objects.get_for_model(
badge_template),
credential_content_type=ContentType.objects.get_for_model(badge_template),
credential_id=badge_template.id,
username=username,
)
100 changes: 67 additions & 33 deletions credentials/apps/badges/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@

See:
"""

import logging
import uuid

from django.dispatch import receiver

from openedx_events.learning.data import BadgeData, BadgeTemplateData
from openedx_events.learning.data import BadgeData, BadgeTemplateData, CoursePassingStatusData, CcxCoursePassingStatusData
from openedx_events.learning.signals import BADGE_AWARDED, BADGE_REVOKED
from openedx_events.tooling import OpenEdxPublicSignal, load_all_signals

from apps.core.api import get_user_by_username
from credentials.apps.core.api import get_user_by_username

from .signals import BADGE_PROGRESS_COMPLETE, BADGE_PROGRESS_INCOMPLETE
from ..services.badge_templates import get_badge_template_by_id
from ..services.user_credentials import create_user_credential
from ..utils import get_badging_event_types
from ..processing import process
from .signals import BADGE_PROGRESS_COMPLETE, BADGE_PROGRESS_INCOMPLETE


logger = logging.getLogger(__name__)
Expand All @@ -32,35 +32,73 @@ def listen_to_badging_events():

for event_type in get_badging_event_types():
signal = OpenEdxPublicSignal.get_signal_by_type(event_type)
signal.connect(event_handler)
signal.connect(processor)


def event_handler(sender, signal, **kwargs):
def processor(sender, signal, **kwargs):
"""
Generic signal handler.
Event bus incoming public signals generic handler.

NOTE (performance): all consumed messages from event bus trigger this.
"""
logger.debug(f"Received signal {signal}")

# NOTE (performance): all consumed messages from event bus trigger this.
process(signal, sender=sender, **kwargs)
# incoming signals (e.g. Messages) processing pipeline:
# - identify user in the Message;
# - check if such user exist (update or create);
# - collect all Requirements for Message event type;
# - no Requirements - nothing to process - STOP;
# - for each found Requirement:
# - see its `effect` (award | revoke)

# AWARD FLOW:
# - check if the related badge template already completed
# - if BadgeProgress exists and BadgeProgress.complete == true >> badge already earned - STOP;
# - check if it is not fulfilled yet
# - if fulfilled (related Fulfillment exists) - STOP;
# - apply payload rules (data-rules);
# - if applied - fulfill the Requirement:
# - create related Fulfillment
# - update of create BadgeProgress
# - BadgeProgress completeness check - check if it was enough for badge earning
# - if BadgeProgress.complete == true
# - emit BADGE_PROGRESS_COMPLETE >> handle_badge_completion
#
# REVOKE FLOW:
# - TBD
# - ...
# - BADGE_PROGRESS_INCOMPLETE emitted >> handle_badge_regression (possibly, we need a flag here)

# hardcode-processing:
course_passing_status = kwargs.get("course_passing_status", None)

if course_passing_status.status == CoursePassingStatusData.PASSING:
BADGE_PROGRESS_COMPLETE.send(sender=sender, username=kwargs.get("user_course_data").user.username, badge_template_id=1) # temporarily faked badge_template_id

if course_passing_status.status == CoursePassingStatusData.FAILING:
BADGE_PROGRESS_INCOMPLETE.send(sender=sender, username=kwargs.get("user_course_data").user.username, badge_template_id=1) # temporarily faked badge_template_id


@receiver(BADGE_PROGRESS_COMPLETE)
def listen_for_completed_badge(sender, username, badge_template_id, **kwargs): # pylint: disable=unused-argument
def handle_badge_completion(sender, username, badge_template_id, **kwargs): # pylint: disable=unused-argument
"""
On user's Badge completion.

- username
- badge template ID
"""

badge_template = get_badge_template_by_id(badge_template_id)
user = get_user_by_username(username)

if badge_template is None:
return

if badge_template.origin == 'openedx':
if badge_template.origin == "openedx":
# instead we need to make badge template type responsible for user credential creation:
# badge_template.award(username) --> creates corresponding type of user-credential
create_user_credential(username, badge_template)

badge = award_badge() # function needs to be implemented

# UserCredential.as_badge_data():

# UserCredential.as_badge_data() - user-credential is responsible for its conversion into payload:
badge_data = BadgeData(
uuid=badge.uuid,
uuid=uuid.uuid1(),
user=user,
template=BadgeTemplateData(
uuid=str(badge_template.uuid),
Expand All @@ -72,24 +110,20 @@ def listen_for_completed_badge(sender, username, badge_template_id, **kwargs):
)

BADGE_AWARDED.send_event(badge=badge_data)


@receiver(BADGE_PROGRESS_INCOMPLETE)
def listen_for_incompleted_badge(sender, username, badge_template_id, **kwargs): # pylint: disable=unused-argument
def handle_badge_regression(sender, username, badge_template_id, **kwargs): # pylint: disable=unused-argument
"""
On user's Badge regression (incompletion).
"""

badge_template = get_badge_template_by_id(badge_template_id)
user = get_user_by_username(username)

if badge_template is None:
return

if user is None:
return

badge = revoke_badge() # function needs to be implemented


# UserCredential.as_badge_data():
badge_data = BadgeData(
uuid=badge.uuid,
uuid=uuid.uuid1(),
user=user,
template=BadgeTemplateData(
uuid=str(badge_template.uuid),
Expand All @@ -99,5 +133,5 @@ def listen_for_incompleted_badge(sender, username, badge_template_id, **kwargs):
image_url=badge_template.icon.url,
),
)

BADGE_REVOKED.send_event(badge=badge_data)
4 changes: 1 addition & 3 deletions credentials/apps/badges/signals/signals.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from django.dispatch import Signal

"""
define internal signals:
Badges internal signals:
- BADGE_REQUIREMENT_FULFILLED - a single specific requirement has finished;
- BADGE_REQUIREMENTS_COMPLETE - all badge template requirements are finished;
- BADGE_REQUIREMENTS_NOT_COMPLETE - a reason for earned badge revocation;
Expand Down
25 changes: 0 additions & 25 deletions credentials/apps/credentials/handlers.py

This file was deleted.

6 changes: 2 additions & 4 deletions credentials/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,7 @@
BADGES_CONFIG = {
# these events become available in rules setup:
"events": [
"org.openedx.learning.course.grade.now.passed.v1",
"org.openedx.learning.course.grade.now.failed.v1",
"org.openedx.learning.ccx.course.grade.now.passed.v1",
"org.openedx.learning.ccx.course.grade.now.failed.v1",
"org.openedx.learning.course.passing.status.v1",
"org.openedx.learning.ccx.course.passing.status.v1",
],
}
Loading