Skip to content

Commit

Permalink
feat: add features dict to EnterpriseCustomerViewSet (#1877)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz authored Sep 20, 2023
1 parent 3284586 commit 8d4eb32
Show file tree
Hide file tree
Showing 17 changed files with 323 additions and 70 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Change Log
Unreleased
----------

[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]
--------
feat: enterprise sso orchestrator api client implementation
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 @@
Enterprise subsidy enrollments and entitlements
===============================================

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.2.0"
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
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
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(),
}
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
44 changes: 36 additions & 8 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ bleach==6.0.0
# -r requirements/test-master.txt
# -r requirements/test.txt
build==1.0.3
# readme-renderer
# via pip-tools
celery==5.3.4
# via
Expand Down Expand Up @@ -170,6 +169,7 @@ code-annotations==1.5.0
# -r requirements/test-master.txt
# -r requirements/test.txt
# edx-lint
# edx-toggles
coverage[toml]==7.3.1
# via
# -r requirements/test.txt
Expand All @@ -180,6 +180,7 @@ cryptography==38.0.4
# -r requirements/test-master.txt
# -r requirements/test.txt
# django-fernet-fields-v2
# jwcrypto
# pgpy
# pyjwt
# pyopenssl
Expand All @@ -192,6 +193,12 @@ defusedxml==0.7.1
# -r requirements/test-master.txt
# -r requirements/test.txt
# djangorestframework-xml
deprecated==1.2.14
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
# jwcrypto
diff-cover==7.7.0
# via -r requirements/test.txt
dill==0.3.7
Expand All @@ -218,6 +225,7 @@ django==3.2.21
# edx-drf-extensions
# edx-i18n-tools
# edx-rbac
# edx-toggles
# jsonfield
django-cache-memoize==0.1.10
# via
Expand All @@ -241,6 +249,7 @@ django-crum==0.7.9
# -r requirements/test.txt
# edx-django-utils
# edx-rbac
# edx-toggles
django-fernet-fields-v2==0.9
# via
# -r requirements/doc.txt
Expand All @@ -251,7 +260,7 @@ django-filter==23.2
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
django-ipware==4.0.2
django-ipware==5.0.0
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
Expand All @@ -267,12 +276,12 @@ django-multi-email-field==0.7.0
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
django-oauth-toolkit==1.4.1
django-oauth-toolkit==1.5.0
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
django-object-actions==4.1.0
django-object-actions==4.2.0
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
Expand All @@ -290,6 +299,7 @@ django-waffle==4.0.0
# -r requirements/test.txt
# edx-django-utils
# edx-drf-extensions
# edx-toggles
djangorestframework==3.14.0
# via
# -r requirements/doc.txt
Expand Down Expand Up @@ -332,13 +342,14 @@ edx-django-utils==5.7.0
# django-config-models
# edx-drf-extensions
# edx-rest-api-client
# edx-toggles
edx-drf-extensions==8.9.2
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
# edx-rbac
edx-i18n-tools==1.1.0
edx-i18n-tools==1.2.0
# via -r requirements/dev.in
edx-lint==5.3.4
# via -r requirements/dev.in
Expand All @@ -363,6 +374,11 @@ edx-tincan-py35==1.0.0
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
edx-toggles==5.1.0
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
factory-boy==3.3.0
# via
# -c requirements/constraints.txt
Expand Down Expand Up @@ -436,6 +452,12 @@ jsonfield==3.1.0
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
jwcrypto==1.5.0
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
# django-oauth-toolkit
kombu==5.3.2
# via
# -r requirements/doc.txt
Expand Down Expand Up @@ -588,7 +610,7 @@ pycryptodomex==3.18.0
# -r requirements/test-master.txt
# -r requirements/test.txt
# snowflake-connector-python
pydata-sphinx-theme==0.13.3
pydata-sphinx-theme==0.14.0
# via
# -r requirements/doc.txt
# sphinx-book-theme
Expand Down Expand Up @@ -734,6 +756,7 @@ six==1.16.0
# -r requirements/test-master.txt
# -r requirements/test.txt
# bleach
# django-oauth-toolkit
# edx-drf-extensions
# edx-lint
# edx-rbac
Expand All @@ -753,7 +776,7 @@ snowballstemmer==2.2.0
# -r requirements/doc.txt
# pydocstyle
# sphinx
snowflake-connector-python==3.1.1
snowflake-connector-python==3.2.0
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
Expand Down Expand Up @@ -927,7 +950,12 @@ wheel==0.41.2
# -r requirements/dev.in
# pip-tools
wrapt==1.15.0
# via astroid
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
# astroid
# deprecated
yarl==1.9.2
# via
# -r requirements/doc.txt
Expand Down
Loading

0 comments on commit 8d4eb32

Please sign in to comment.