Skip to content

Commit

Permalink
feat: sync funcs with @workflow_action_step decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz committed Oct 1, 2024
1 parent 44f981e commit d6149c8
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 60 deletions.
28 changes: 22 additions & 6 deletions enterprise_access/apps/workflows/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@
Admin for workflows app.
"""
import json
from django.utils.safestring import mark_safe

from django import forms
from django.contrib import admin
from django.forms.models import inlineformset_factory
from django.http.request import HttpRequest
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from djangoql.admin import DjangoQLSearchMixin
from ordered_model.admin import OrderedInlineModelAdminMixin, OrderedStackedInline

from enterprise_access.apps.workflows import models
from enterprise_access.apps.workflows.registry import WorkflowActionRegistry


class WorkflowExecutionStatusInline(admin.TabularInline):
Expand All @@ -26,7 +25,9 @@ class WorkflowExecutionStatusInline(admin.TabularInline):
def has_add_permission(self, request, obj=None): # pylint: disable=unused-argument
return False

def has_delete_permission(self, request, obj=None, workflow_execution_status=None): # pylint: disable=unused-argument
def has_delete_permission(
self, request, obj=None, workflow_execution_status=None
): # pylint: disable=unused-argument
return False

def admin_link(self, instance):
Expand Down Expand Up @@ -159,6 +160,21 @@ class WorkflowActionStepAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
"""Admin class for the WorkflowActionStep model."""
list_display = ('name', 'action_reference')
search_fields = ('name', 'action_reference')
fields = ('name', 'action_reference', 'created', 'modified')
readonly_fields = fields
ordering = ['-created', 'name'] # Order by modified date first, then by name

def has_add_permission(self, request): # pylint: disable=unused-argument
"""Disallow adding new WorkflowActionSteps."""
return False

def has_delete_permission(self, request, obj=None): # pylint: disable=unused-argument
"""Disallow deleting WorkflowActionSteps."""
return False

def has_change_permission(self, request, obj=None): # pylint: disable=unused-argument
"""Disallow editing WorkflowActionSteps, making them read-only."""
return False


@admin.register(models.WorkflowGroupActionStepThrough)
Expand All @@ -175,7 +191,7 @@ class WorkflowExecutionStepStatusAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
search_fields = ('id', 'workflow_execution__uuid', 'step__name', 'task_id')
list_filter = ('status', 'workflow_execution__workflow_definition')
fields = (
'id', 'workflow_execution', 'step', 'status',
'id', 'workflow_execution', 'step', 'status',
'task_id', 'formatted_result', 'error_message',
'created', 'modified',
)
Expand Down
19 changes: 19 additions & 0 deletions enterprise_access/apps/workflows/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,22 @@ class WorkflowsConfig(AppConfig):
"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'enterprise_access.apps.workflows'

def ready(self):
"""
Import decorated workflow handlers when the app is ready
"""
# pylint: disable=unused-import, import-outside-toplevel
import enterprise_access.apps.workflows.handlers

# Perform cleanup of the registry at startup
self.cleanup_registry()

def cleanup_registry(self):
"""
Cleans up the action registry by removing any WorkflowActionSteps that no longer
exist in the registered action list. This is called on app startup.
"""
# pylint: disable=import-outside-toplevel
from enterprise_access.apps.workflows.registry import WorkflowActionRegistry
WorkflowActionRegistry.cleanup_registry()
38 changes: 38 additions & 0 deletions enterprise_access/apps/workflows/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Decorators for workflows app.
"""

from django.db.models import Q

from enterprise_access.apps.workflows.models import WorkflowActionStep
from enterprise_access.apps.workflows.registry import WorkflowActionRegistry


def workflow_action_step(slug, name):
"""
A single decorator that registers the workflow action with the registry
and ensures that a WorkflowActionStep exists in the database.
:param slug: Unique identifier for the workflow action step.
:param name: Human-readable name for the workflow action step.
"""
def decorator(func):
# Register the action step in the workflow registry
WorkflowActionRegistry.register_action_step(slug, name)(func)

# Check if the action step already exists and whether its name has changed
existing_step = WorkflowActionStep.objects.filter(
Q(action_reference=slug) & Q(name=name)
).first()

if not existing_step:
# Only update or create if the existing step was not found
WorkflowActionStep.objects.update_or_create(
action_reference=slug,
defaults={"name": name}
)

# Return the original function to allow it to be used as normal
return func

return decorator
82 changes: 52 additions & 30 deletions enterprise_access/apps/workflows/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,77 @@
Default workflow handlers.
"""

from enterprise_access.apps.workflows.decorators import workflow_action_step

def activate_enterprise_customer_user():

@workflow_action_step(slug='activate_enterprise_customer_user', name='Activate Enterprise Customer User')
def activate_enterprise_customer_user_changed():
"""
Activates an enterprise customer user for the specified enterprise customer.
"""
print("Activating enterprise customer user for enterprise customer UUID: TBD")
return {
"id":692281,
"enterpriseCustomer":{
"uuid":"852eac48-b5a9-4849-8490-743f3f2deabf",
"name":"Executive Education (2U) Integration QA",
"slug":"exec-ed-2u-integration-qa",
"active":True,
"id": 692281,
"enterpriseCustomer": {
"uuid": "852eac48-b5a9-4849-8490-743f3f2deabf",
"name": "Executive Education (2U) Integration QA",
"slug": "exec-ed-2u-integration-qa",
"active": True,
"other": "fields",
},
"active":True,
"userId":17737721,
"user":{
"id":17737721,
"username":"astankiewicz_edx",
"firstName":"Adam",
"lastName":"Stankiewicz",
"email":"[email protected]",
"isStaff":True,
"isActive":True,
"dateJoined":"2018-01-26T20:05:56Z"
"active": True,
"userId": 17737721,
"user": {
"id": 17737721,
"username": "astankiewicz_edx",
"firstName": "Adam",
"lastName": "Stankiewicz",
"email": "[email protected]",
"isStaff": True,
"isActive": True,
"dateJoined": "2018-01-26T20:05:56Z"
},
"dataSharingConsentRecords":[
"dataSharingConsentRecords": [
{
"username":"astankiewicz_edx",
"enterpriseCustomerUuid":"852eac48-b5a9-4849-8490-743f3f2deabf",
"exists":True,
"consentProvided":True,
"consentRequired":False,
"courseId":"course-v1:HarvardX+CS50+X"
"username": "astankiewicz_edx",
"enterpriseCustomerUuid": "852eac48-b5a9-4849-8490-743f3f2deabf",
"exists": True,
"consentProvided": True,
"consentRequired": False,
"courseId": "course-v1:HarvardX+CS50+X"
}
],
"groups":[],
"created":"2022-11-08T14:49:09.494221Z",
"inviteKey":"b584d15d-f286-4b25-b6da-766bab654394",
"roleAssignments":["enterprise_learner"],
"enterpriseGroup":[]
"groups": [],
"created": "2022-11-08T14:49:09.494221Z",
"inviteKey": "b584d15d-f286-4b25-b6da-766bab654394",
"roleAssignments": ["enterprise_learner"],
"enterpriseGroup": []
}


@workflow_action_step(slug='activate_subscription_license', name='Activate Subscription License')
def activate_subscription_license():
"""
Activates a subscription license for the specified subscription license.
"""
print("Activating subscription license for subscription license UUID: TBD")


@workflow_action_step(slug='auto_apply_subscription_license', name='Auto-apply Subscription License')
def auto_apply_subscription_license():
"""
Automatically applies a subscription license to an enterprise customer user.
"""
print("Automatically applying subscription license to enterprise customer user.")


@workflow_action_step(slug='retrieve_subscription_licenses', name='Retrieve Subscription Licenses')
def retrieve_subscription_licenses():
"""
Retrieves a subscription license for the specified enterprise customer.
"""
print("Retrieving subscription license for enterprise customer UUID: TBD")
# learner-licenses (license-manager)
return {
"count": 1,
"current_page": 1,
Expand Down Expand Up @@ -117,6 +124,7 @@ def retrieve_subscription_licenses():
}


@workflow_action_step(slug='retrieve_credits_available', name='Retrieve Credits Available')
def retrieve_credits_available():
"""
Retrieves the number of credits available for the specified enterprise customer.
Expand All @@ -140,17 +148,31 @@ def retrieve_credits_available():
]


@workflow_action_step(
slug='enroll_default_enterprise_course_enrollments',
name='Enroll Default Enterprise Course Enrollments',
)
def enroll_default_enterprise_course_enrollments():
"""
Enrolls an enterprise customer user in default enterprise course enrollments.
"""
# 1. Fetch GET /enterprise/api/v1/enterprise-customer/{uuid}/default-course-enrollments/with-enrollment-status/
#
# 2. Determine redeemability of each not-yet-enrolled default enterprise course enrollment with subscription license
# or Learner Credit.
#
# 3. Redeem/enroll the enterprise customer user in the not-yet-enrolled and redeemable default enterprise course
# enrollments, using the appropriate redemption method.
# - For subscriptions redemption (VSF):
# * Either call `enroll_learners_in_courses` (edx-enterprise) directly OR consider the
# implications of getting subscriptions into the can-redeem / redeem paradigm (redeem
# uses same `enroll_learners_in_courses` already).
# - For Learner Credit redemption:
# * Redeem with existing can-redeem / redeem paradigm.

print("Enrolling enterprise customer user in default enterprise course enrollments.")


@workflow_action_step(slug='retrieve_enterprise_course_enrollments', name='Retrieve Enterprise Course Enrollments')
def retrieve_enterprise_course_enrollments():
print("Retrieving enterprise course enrollments for enterprise customer user.")
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
from django.core.management.base import BaseCommand

from enterprise_access.apps.workflows.models import (
WorkflowDefinition,
WorkflowActionStep,
WorkflowDefinition,
WorkflowGroupActionStepThrough,
WorkflowItemThrough,
WorkflowStepGroup,
WorkflowStepGroup
)

logger = logging.getLogger(__name__)


class Command(BaseCommand):
"""
Seed the database with test workflows.
Expand Down
20 changes: 7 additions & 13 deletions enterprise_access/apps/workflows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
Models for the workflows app.
"""

import importlib
import collections
import importlib
import logging
from uuid import uuid4

Expand All @@ -18,6 +18,7 @@
from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet

from enterprise_access.apps.workflows.constants import WorkflowStatus
from enterprise_access.apps.workflows.registry import WorkflowActionRegistry
from enterprise_access.utils import localized_utcnow

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -325,20 +326,14 @@ class WorkflowActionStep(TimeStampedModel):

def clean(self):
"""
Validates the action reference to ensure that the specified action exists
and can be imported dynamically.
Validates that the action reference exists in the registry.
"""
try:
module_name, function_name = self.action_reference.rsplit('.', 1)
module = importlib.import_module(module_name)
if not hasattr(module, function_name):
raise ImportError
except (ImportError, ValueError) as exc:
raise ValidationError(f"Invalid action_reference: {self.action_reference} does not exist.") from exc
if not WorkflowActionRegistry.get(self.action_reference):
raise ValidationError(f"Action reference '{self.action_reference}' is not registered.")

def __str__(self):
"""Returns the step name and the workflow it belongs to, including its order."""
return self.name
"""Returns the step name and its action reference."""
return f"Step: {self.name} (Action: {self.action_reference})"


class WorkflowDefinitionQuerySet(OrderedModelQuerySet):
Expand Down Expand Up @@ -554,7 +549,6 @@ def __str__(self):
return f"{self.id}"



class WorkflowItemThrough(OrderedModel, TimeStampedModel):
"""
Unified model to order both WorkflowActionSteps and WorkflowStepGroups within a WorkflowDefinition.
Expand Down
Loading

0 comments on commit d6149c8

Please sign in to comment.