From fc3bc032b981230ee61c5572e21fb23b7c892763 Mon Sep 17 00:00:00 2001
From: Matt Tuchfarber <mtuchfarber@edx.org>
Date: Fri, 13 Mar 2020 15:14:37 -0400
Subject: [PATCH] Update with suggestions: - Add ADR describing Plugin Contexts
 - Remove app-specific constants from framework-level code - Add dashboard
 constants to student app with README

---
 common/djangoapps/student/README.rst         |  5 ++
 common/djangoapps/student/api.py             |  1 +
 common/djangoapps/student/views/dashboard.py |  3 +-
 docs/decisions/0003-plugin-contexts.rst      | 72 ++++++++++++++++++++
 openedx/core/djangoapps/plugins/constants.py |  8 ---
 5 files changed, 80 insertions(+), 9 deletions(-)
 create mode 100644 docs/decisions/0003-plugin-contexts.rst

diff --git a/common/djangoapps/student/README.rst b/common/djangoapps/student/README.rst
index e0af6f15f3c..c62ff706caa 100644
--- a/common/djangoapps/student/README.rst
+++ b/common/djangoapps/student/README.rst
@@ -14,3 +14,8 @@ Glossary
 
 More Documentation
 ==================
+
+Plugins
+-------
+Plugin Context view names (see ADR 0003-plugin-contexts.rst):
+* "course_dashboard" -> student.views.dashboard.student_dashboard
diff --git a/common/djangoapps/student/api.py b/common/djangoapps/student/api.py
index 2d372ed514a..f1c7d486f38 100644
--- a/common/djangoapps/student/api.py
+++ b/common/djangoapps/student/api.py
@@ -42,6 +42,7 @@ MANUAL_ENROLLMENT_ROLE_CHOICES = configuration_helpers.get_value(
     settings.MANUAL_ENROLLMENT_ROLE_CHOICES
 )
 
+COURSE_DASHBOARD_PLUGIN_VIEW_NAME = "course_dashboard"
 
 def create_manual_enrollment_audit(
     enrolled_by,
diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py
index 96a57c69348..b85782452d9 100644
--- a/common/djangoapps/student/views/dashboard.py
+++ b/common/djangoapps/student/views/dashboard.py
@@ -25,6 +25,7 @@ from opaque_keys.edx.keys import CourseKey
 from pytz import UTC
 from shoppingcart.models import CourseRegistrationCode, DonationConfiguration
 from six import iteritems, text_type
+from student.api import COURSE_DASHBOARD_PLUGIN_VIEW_NAME
 from student.helpers import cert_info, check_verify_status_by_course, get_resume_urls_for_enrollments
 from student.models import (
     AccountRecovery,
@@ -884,7 +885,7 @@ def student_dashboard(request):
 
     context_from_plugins = get_plugins_view_context(
         plugin_constants.ProjectType.LMS,
-        plugin_constants.PluginContexts.VIEWS.STUDENT_DASHBOARD,
+        COURSE_DASHBOARD_PLUGIN_VIEW_NAME,
         context
     )
     context.update(context_from_plugins)
diff --git a/docs/decisions/0003-plugin-contexts.rst b/docs/decisions/0003-plugin-contexts.rst
new file mode 100644
index 00000000000..386a0f1f0a9
--- /dev/null
+++ b/docs/decisions/0003-plugin-contexts.rst
@@ -0,0 +1,72 @@
+Plugin Contexts
+---------------
+
+Status
+======
+Draft
+
+Context
+=======
+edx-platform contains a plugin system which allows new Django apps to be installed inside the LMS and Studio without requiring the LMS/Studio to know about them. This is what enables us to move to a small and extensible core. While we had the ability to add settings, URLs, and signal handlers in our plugins, we wasn't a any way for a plugin to affect the commonly used pages that the core was delivering. Thus a plugin couldn't change any details on the dashboard, courseware, or any other rendered page that the platform delivered.
+
+Decisions
+=========
+We have added the ability to add page context additions to the plugin system. This means that a plugin will be able to add context any view where it is enabled. To support this we have decided:
+# Plugins will define a callable function that the LMS and/or studio can import and call, which will return additional context to be added.
+# Every page that a plugin wants to add context to, must add a line to add the plugin contexts directly before the render.
+# All view + plugin data will exist in the same dictionary. To better protect against dictionary key collisions, it is suggested that you prefix your new context items with your app name (e.g. return {"myapp__some_variable": True} vs {"some_variable": True})
+# Each view will have a constant name that will be defined within it's app's API.py which will be used by plugins. These must be globally unique. These will also be recorded in the rendering app's README.rst file.
+# Plugin app's may import the view name from the rendering app's api.py or just use it's string.
+# For now, in order to use these new context data items, we must use theming alongside this to keep the new context out of the core. This may be iterated on in the future.
+
+Implementation
+--------------
+
+In the plugin app
+~~~~~~~~~~~~~~~~~
+Config
+++++++
+Inside of your AppConfig your new plugin app, add a "context_config" item like below.
+* The format will be {"globally_unique_view_name": "function_inside_plugin_app"}
+* The function name & path don't need to be named anything specific, so long as they work
+* These functions will be called on **every** render of that view, so keep them efficient or memoize them if they aren't user specific.
+
+::
+    class MyAppConfig(AppConfig):
+        name = "my_app"
+
+        plugin_app = {
+            "context_config": {
+                "lms.djangoapp":  {
+                    "course_dashboard": "my_app.context_api.get_dashboard_context"
+                }
+            }
+        }
+
+Function
+++++++++
+The function that will be called by the plugin system should accept a single parameter which will be the previously existing context. It should then return a dictionary which consists of items which will be added to the context
+
+Example:
+::
+    def my_context_function(existing_context):
+        additional_context = {"myapp__some_variable": 10}
+        if existing_context.get("some_value"):
+            additional_context.append({"myapp__some_other_variable": True})
+        return additional_context
+
+
+In the core (LMS / Studio)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+The view you wish to add context to should have the following pieces enabled:
+
+* A constant defined inside the app's for the globally unique view name.
+* The view must call lines similar to the below right before the render so that the plugin has the full context.
+::
+    context_from_plugins = get_plugins_view_context(
+        plugin_constants.ProjectType.LMS,
+        current_app.api.THIS_VIEW_NAME,
+        context
+    )
+    context.update(context_from_plugins)
+
diff --git a/openedx/core/djangoapps/plugins/constants.py b/openedx/core/djangoapps/plugins/constants.py
index 73139795f75..3a2b4d434e0 100644
--- a/openedx/core/djangoapps/plugins/constants.py
+++ b/openedx/core/djangoapps/plugins/constants.py
@@ -77,11 +77,6 @@ class PluginSignals(object):
     RELATIVE_PATH = u'relative_path'
     DEFAULT_RELATIVE_PATH = u'signals'
 
-
-class PluginContextsViews(object):
-    COURSE_DASHBOARD = u'course_dashboard'
-
-
 class PluginContexts(object):
     """
     The PluginContexts enum defines dictionary field names (and defaults)
@@ -89,6 +84,3 @@ class PluginContexts(object):
     additional views it would like to add context into.
     """
     CONFIG = u"context_config"
-
-    VIEWS = PluginContextsViews
-
-- 
GitLab