Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/openedx/edx-enterprise in…
Browse files Browse the repository at this point in the history
…to hamza/ENT-5039
  • Loading branch information
hamzawaleed01 committed Sep 22, 2023
2 parents 66ce31a + 7fbfa8d commit f0d3c8e
Show file tree
Hide file tree
Showing 26 changed files with 819 additions and 590 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ Change Log
Unreleased
----------
[4.3.1]
--------
chore: use lms_update_or_create_enrollment without feature flag

[4.3.0]
--------
feat: Added the ``enable_career_engagement_network_on_learner_portal`` field for EnterpriseCustomer

[4.2.0]
--------
feat: create generic ``PaginationWithFeatureFlags`` to add a ``features`` property to DRF's default pagination response containing Waffle-based feature flags.
feat: integrate ``PaginationWithFeatureFlags`` with ``EnterpriseCustomerViewSet``.

[4.1.15]
--------
Expand Down
43 changes: 43 additions & 0 deletions docs/decisions/0012-enterprise-feature-flags-waffle.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Waffle-based feature flags for Enterprise
=========================================

Status
------

Accepted (September 2023)

Context
-------

Enterprise typically uses environment configuration to control feature flags for soft/dark releases. For micro-frontends, this control involves environment variables that are set at build time and used to compile JavaScript source or configuration settings derived from the runtime MFE configuration API in edx-platform. For backend APIs, this control typically either involves configuration settings or, on occasion, a Waffle flag.

By solely relying on environment configuration, we are unable to dynamically control feature flags in production based on the user's context.

For example, we may want to enable a feature for all staff users but keep it disabled for customers/users while it's in development. Similarly, we may want to enable a feature for a subset of specific users (e.g., members of a specific engineering squad) in production to QA before enabling it for all users.

However, neither of these are really possible with environment configuration.


Decisions
---------

We will adopt the Waffle-based approach to feature flags for Enterprise micro-frontends in favor of environment variables or the MFE runtime configuration API. This approach will allow us to have more dynamic and granual control over feature flags in production based on the user's context (e.g., all staff users have a feature enabled, a subset of users have a feature enabled, etc.).


Consequences
------------

* We are introducing a third mechanism by which we control feature flags in the Enterprise micro-frontends. We may want to consider migrating other feature flags to this Waffle-based approach in the future. Similarly, such an exercise may be a good opportunity to revisit what feature flags exist today and what can be removed now that the associate features are stable in production.
* The majority of the feature flag setup for Enterprise systems lives in configuration settings. By moving more towards Waffle, the feature flag setup will live in databases and modified via Django Admin instead of via code changes.


Further Improvements
--------------------

* To further expand on the capabilities of Waffle-based feature flags, we may want to invest in the ability to enable such feature flags at the **enterprise customer** layer, where we may be able to enable soft-launch features for all users linked to an enterprise customer without needing to introduce new boolean fields on the ``EnterpriseCustomer`` model.

Alternatives Considered
-----------------------

* Continue using the environment variable to enabling feature flags like Enterprise has been doing the past few years. In order to test a disabled feature in production, this approach requires developers to allow the environment variable to be intentionally overridden by a `?feature=` query parameter in the URL. Without the query parameter in the URL, there is no alternative ways to temporarily enable the feature without impacting actual users and customers. Waffle-based feature flags give much more flexibility to developers to test features in production without impacting actual users and customers.
* Exposes a net-new API endpoint specific to returning feature flags for Enterprise needs. This approach was not adopted as it would require new API integrations within both the enterprise administrator and learner portal micro-frontends, requiring users to wait for additional network requests to resolve. Instead, we are preferring to include Waffle-based feature flags on existing API endpoints made by both micro-frontends.
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.1.15"
__version__ = "4.3.1"
4 changes: 3 additions & 1 deletion enterprise/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ class EnterpriseCustomerAdmin(DjangoObjectActions, SimpleHistoryAdmin):
'enable_integrated_customer_learner_portal_search',
'enable_analytics_screen', 'enable_audit_enrollment',
'enable_audit_data_reporting', 'enable_learner_portal_offers',
'enable_executive_education_2U_fulfillment'),
'enable_executive_education_2U_fulfillment',
'enable_career_engagement_network_on_learner_portal',
'career_engagement_network_message'),
'description': ('The following default settings should be the same for '
'the majority of enterprise customers, '
'and are either rarely used, unlikely to be sold, '
Expand Down
2 changes: 2 additions & 0 deletions enterprise/admin/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ class Meta:
"enable_executive_education_2U_fulfillment",
"hide_labor_market_data",
"enable_integrated_customer_learner_portal_search",
"enable_career_engagement_network_on_learner_portal",
"career_engagement_network_message",
"enable_analytics_screen",
"enable_portal_reporting_config_screen",
"enable_portal_saml_configuration_screen",
Expand Down
31 changes: 31 additions & 0 deletions enterprise/api/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,39 @@
from collections import OrderedDict
from urllib.parse import urlparse

from edx_rest_framework_extensions.paginators import DefaultPagination
from rest_framework.response import Response

from enterprise.toggles import enterprise_features


class PaginationWithFeatureFlags(DefaultPagination):
"""
Adds a ``features`` dictionary to the default paginated response
provided by edx_rest_framework_extensions. The ``features`` dict
represents a collection of Waffle-based feature flags/samples/switches
that may be used to control whether certain aspects of the system are
enabled or disabled (e.g., feature flag turned on for all staff users but
not turned on for real customers/learners).
"""

def get_paginated_response(self, data):
"""
Modifies the default paginated response to include ``enterprise_features`` dict.
Arguments:
self: PaginationWithFeatureFlags instance.
data (dict): Results for current page.
Returns:
(Response): DRF response object containing ``enterprise_features`` dict.
"""
paginated_response = super().get_paginated_response(data)
paginated_response.data.update({
'enterprise_features': enterprise_features(),
})
return paginated_response


def get_paginated_response(data, request):
"""
Expand Down
3 changes: 2 additions & 1 deletion enterprise/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ class Meta:
'enable_integrated_customer_learner_portal_search', 'enable_generation_of_api_credentials',
'enable_portal_lms_configurations_screen', 'sender_alias', 'identity_providers',
'enterprise_customer_catalogs', 'reply_to', 'enterprise_notification_banner', 'hide_labor_market_data',
'modified', 'enable_universal_link', 'enable_browse_and_request', 'admin_users'
'modified', 'enable_universal_link', 'enable_browse_and_request', 'admin_users',
'enable_career_engagement_network_on_learner_portal', 'career_engagement_network_message'
)

identity_providers = EnterpriseCustomerIdentityProviderSerializer(many=True, read_only=True)
Expand Down
2 changes: 2 additions & 0 deletions enterprise/api/v1/views/enterprise_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from enterprise import models
from enterprise.api.filters import EnterpriseLinkedUserFilterBackend
from enterprise.api.pagination import PaginationWithFeatureFlags
from enterprise.api.throttles import HighServiceUserThrottle
from enterprise.api.v1 import serializers
from enterprise.api.v1.decorators import require_at_least_one_query_parameter
Expand Down Expand Up @@ -54,6 +55,7 @@ class EnterpriseCustomerViewSet(EnterpriseReadWriteModelViewSet):
queryset = models.EnterpriseCustomer.active_customers.all()
serializer_class = serializers.EnterpriseCustomerSerializer
filter_backends = EnterpriseReadWriteModelViewSet.filter_backends + (EnterpriseLinkedUserFilterBackend,)
pagination_class = PaginationWithFeatureFlags

USER_ID_FILTER = 'enterprise_customer_users__user_id'
FIELDS = (
Expand Down
33 changes: 33 additions & 0 deletions enterprise/migrations/0185_auto_20230921_1007.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 3.2.21 on 2023-09-21 10:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise', '0184_auto_20230914_2057'),
]

operations = [
migrations.AddField(
model_name='enterprisecustomer',
name='career_engagement_network_message',
field=models.TextField(blank=True, help_text='Message text shown on the learner portal dashboard for career engagement network.'),
),
migrations.AddField(
model_name='enterprisecustomer',
name='enable_career_engagement_network_on_learner_portal',
field=models.BooleanField(default=False, help_text='If checked, the learners will be able to see the link to CEN on the learner portal dashboard.', verbose_name='Allow navigation to career engagement network from learner portal dashboard'),
),
migrations.AddField(
model_name='historicalenterprisecustomer',
name='career_engagement_network_message',
field=models.TextField(blank=True, help_text='Message text shown on the learner portal dashboard for career engagement network.'),
),
migrations.AddField(
model_name='historicalenterprisecustomer',
name='enable_career_engagement_network_on_learner_portal',
field=models.BooleanField(default=False, help_text='If checked, the learners will be able to see the link to CEN on the learner portal dashboard.', verbose_name='Allow navigation to career engagement network from learner portal dashboard'),
),
]
15 changes: 15 additions & 0 deletions enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,14 @@ class Meta:
)
)

enable_career_engagement_network_on_learner_portal = models.BooleanField(
verbose_name="Allow navigation to career engagement network from learner portal dashboard",
default=False,
help_text=_(
"If checked, the learners will be able to see the link to CEN on the learner portal dashboard."
)
)

enable_analytics_screen = models.BooleanField(
verbose_name="Display analytics page",
default=True,
Expand Down Expand Up @@ -445,6 +453,13 @@ class Meta:
default=False,
)

career_engagement_network_message = models.TextField(
blank=True,
help_text=_(
'Message text shown on the learner portal dashboard for career engagement network.'
),
)

@property
def enterprise_customer_identity_provider(self):
"""
Expand Down
36 changes: 36 additions & 0 deletions enterprise/toggles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Waffle toggles for enterprise features within the LMS.
"""

from edx_toggles.toggles import WaffleFlag

ENTERPRISE_NAMESPACE = 'enterprise'
ENTERPRISE_LOG_PREFIX = 'Enterprise: '

# .. toggle_name: enterprise.TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Enables top-down assignment
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2023-09-15
TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM = WaffleFlag(
f'{ENTERPRISE_NAMESPACE}.top_down_assignment_real_time_lcm',
__name__,
ENTERPRISE_LOG_PREFIX,
)


def top_down_assignment_real_time_lcm():
"""
Returns whether top-down assignment and real time LCM feature flag is enabled.
"""
return TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM.is_enabled()


def enterprise_features():
"""
Returns a dict of enterprise Waffle-based feature flags.
"""
return {
'top_down_assignment_real_time_lcm': top_down_assignment_real_time_lcm(),
}
34 changes: 8 additions & 26 deletions enterprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,8 @@
from enterprise.logging import getEnterpriseLogger

try:
from openedx.features.enterprise_support.enrollments.utils import (
lms_enroll_user_in_course,
lms_update_or_create_enrollment,
)
from openedx.features.enterprise_support.enrollments.utils import lms_update_or_create_enrollment
except ImportError:
lms_enroll_user_in_course = None
lms_update_or_create_enrollment = None

try:
Expand Down Expand Up @@ -1824,30 +1820,16 @@ def customer_admin_enroll_user_with_status(
succeeded = False
new_enrollment = False
enterprise_fulfillment_source_uuid = None
emet_enable_auto_upgrade_enrollment_mode = getattr(
settings,
'ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE',
False,
)
try:
# enrolls a user in a course per LMS flow, but this method doesn't create enterprise records
# yet so we need to create it immediately after calling lms_update_or_create_enrollment.
if emet_enable_auto_upgrade_enrollment_mode:
new_enrollment = lms_update_or_create_enrollment(
user.username,
course_id,
course_mode,
is_active=True,
enterprise_uuid=enterprise_customer.uuid,
)
else:
new_enrollment = lms_enroll_user_in_course(
user.username,
course_id,
course_mode,
enterprise_customer.uuid,
is_active=True,
)
new_enrollment = lms_update_or_create_enrollment(
user.username,
course_id,
course_mode,
is_active=True,
enterprise_uuid=enterprise_customer.uuid,
)
succeeded = True
LOGGER.info("Successfully enrolled user %s in course %s", user.id, course_id)
except (CourseEnrollmentError, CourseUserGroup.DoesNotExist) as error:
Expand Down
3 changes: 2 additions & 1 deletion requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ edx-django-utils>=3.12.0
edx-drf-extensions
edx-opaque-keys[django]
edx-rest-api-client
edx-tincan-py35
edx-rbac
edx-tincan-py35
edx-toggles
jsondiff
jsonfield
path.py
Expand Down
4 changes: 1 addition & 3 deletions requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#
distlib==0.3.7
# via virtualenv
filelock==3.12.3
filelock==3.12.4
# via
# tox
# virtualenv
Expand All @@ -30,7 +30,5 @@ tox==3.28.0
# tox-battery
tox-battery==0.6.1
# via -r requirements/ci.in
typing-extensions==4.7.1
# via filelock
virtualenv==20.24.5
# via tox
3 changes: 3 additions & 0 deletions requirements/common_constraints.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@




# A central location for most common version constraints
# (across edx repos) for pip-installation.
#
Expand Down
Loading

0 comments on commit f0d3c8e

Please sign in to comment.