From 2842533eb4ff73a7f59a10dea81a8bddf959cceb Mon Sep 17 00:00:00 2001
From: Andy Armstrong <andya@edx.org>
Date: Tue, 20 Jun 2017 19:03:37 -0400
Subject: [PATCH] Introduce a course_tool plugin entry point and migrate the
 updates, bookmarks and reviews tools into plugins.

---
 openedx/features/course_bookmarks/plugins.py  | 40 ++++++++++
 .../course_experience/course_tools.py         | 67 ++++++++++++++++
 openedx/features/course_experience/plugins.py | 79 +++++++++++++++++++
 .../course-home-fragment.html                 | 48 +++++------
 setup.py                                      | 13 +--
 5 files changed, 215 insertions(+), 32 deletions(-)
 create mode 100644 openedx/features/course_bookmarks/plugins.py
 create mode 100644 openedx/features/course_experience/course_tools.py
 create mode 100644 openedx/features/course_experience/plugins.py

diff --git a/openedx/features/course_bookmarks/plugins.py b/openedx/features/course_bookmarks/plugins.py
new file mode 100644
index 00000000000..3f24942ae8b
--- /dev/null
+++ b/openedx/features/course_bookmarks/plugins.py
@@ -0,0 +1,40 @@
+"""
+Platform plugins to support course bookmarks.
+"""
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+from openedx.features.course_experience.course_tools import CourseTool
+
+
+class CourseBookmarksTool(CourseTool):
+    """
+    The course bookmarks tool.
+    """
+    @classmethod
+    def is_enabled(cls, course_key):
+        """
+        Always show the bookmarks tool.
+        """
+        return True
+
+    @classmethod
+    def title(cls):
+        """
+        Returns the title of this tool.
+        """
+        return _('Bookmarks')
+
+    @classmethod
+    def icon_classes(cls):
+        """
+        Returns the icon classes needed to represent this tool.
+        """
+        return 'fa fa-bookmark'
+
+    @classmethod
+    def url(cls, course_key):
+        """
+        Returns the URL for this tool for the specified course key.
+        """
+        return reverse('openedx.course_bookmarks.home', args=[course_key])
diff --git a/openedx/features/course_experience/course_tools.py b/openedx/features/course_experience/course_tools.py
new file mode 100644
index 00000000000..bf36e14fe68
--- /dev/null
+++ b/openedx/features/course_experience/course_tools.py
@@ -0,0 +1,67 @@
+"""
+Support for course tool plugins.
+"""
+from openedx.core.lib.api.plugins import PluginManager
+
+# Stevedore extension point namespace
+COURSE_TOOLS_NAMESPACE = 'openedx.course_tool'
+
+
+class CourseTool(object):
+    """
+    This is an optional base class for Course Tool plugins.
+
+    Plugin implementations inside this repo should subclass CourseTool to get
+    useful default behavior, and to add clarity to the code.  This base class is
+    not a requirement, and plugin implementations outside of this repo should
+    simply follow the contract defined below.
+    """
+
+    @classmethod
+    def is_enabled(cls, course_key):
+        """
+        Returns true if this tool is enabled for the specified course key.
+        """
+        return True
+
+    @classmethod
+    def title(cls, course_key):
+        """
+        Returns the title for the course tool.
+        """
+        raise NotImplementedError("Must specify a title for a course tool.")
+
+    @classmethod
+    def icon_classes(cls, course_key):
+        """
+        Returns the icon classes needed to represent this tool.
+        
+        For example, return an icon from font-awasome.css, like 'fa fa-star'.
+        """
+        raise NotImplementedError("Must specify an icon for a course tool.")
+
+    @classmethod
+    def url(cls, course_key):
+        """
+        Returns the URL for this tool for the specified course key.
+        """
+        raise NotImplementedError("Must specify a url for a course tool.")
+
+
+class CourseToolsPluginManager(PluginManager):
+    """
+    Manager for all of the course tools that have been made available.
+
+    Course tool implementation can subclass `CourseTool` or can implement
+    the required class methods themselves.
+    """
+    NAMESPACE = COURSE_TOOLS_NAMESPACE
+
+    @classmethod
+    def get_course_tools(cls):
+        """
+        Returns the list of available course tools in their canonical order.
+        """
+        course_tools = cls.get_available_plugins().values()
+        course_tools.sort(key=lambda course_tool: course_tool.title())
+        return course_tools
diff --git a/openedx/features/course_experience/plugins.py b/openedx/features/course_experience/plugins.py
new file mode 100644
index 00000000000..611c3483821
--- /dev/null
+++ b/openedx/features/course_experience/plugins.py
@@ -0,0 +1,79 @@
+"""
+Platform plugins to support the course experience.
+
+This includes any locally defined CourseTools.
+"""
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+
+from . import UNIFIED_COURSE_TAB_FLAG, SHOW_REVIEWS_TOOL_FLAG
+from views.course_reviews import CourseReviewsModuleFragmentView
+from course_tools import CourseTool
+
+
+class CourseUpdatesTool(CourseTool):
+    """
+    The course updates tool.
+    """
+    @classmethod
+    def title(cls):
+        """
+        Returns the title of this tool.
+        """
+        return _('Updates')
+
+    @classmethod
+    def icon_classes(cls):
+        """
+        Returns icon classes needed to represent this tool.
+        """
+        return 'fa fa-newspaper-o'
+
+    @classmethod
+    def is_enabled(cls, course_key):
+        """
+        Returns True if this tool is enabled for the specified course key.
+        """
+        return UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key)
+
+    @classmethod
+    def url(cls, course_key):
+        """
+        Returns the URL for this tool for the specified course key.
+        """
+        return reverse('openedx.course_experience.course_updates', args=[course_key])
+
+
+class CourseReviewsTool(CourseTool):
+    """
+    The course reviews tool.
+    """
+    @classmethod
+    def title(cls):
+        """
+        Returns the title of this tool.
+        """
+        return _('Reviews')
+
+    @classmethod
+    def icon_classes(cls):
+        """
+        Returns icon classes needed to represent this tool.
+        """
+        return 'fa fa-star'
+
+    @classmethod
+    def is_enabled(cls, course_key):
+        """
+        Returns True if this tool is enabled for the specified course key.
+        """
+        reviews_configured = CourseReviewsModuleFragmentView.is_configured()
+        return SHOW_REVIEWS_TOOL_FLAG.is_enabled(course_key) and reviews_configured
+
+    @classmethod
+    def url(cls, course_key):
+        """
+        Returns the URL for this tool for the specified course key.
+        """
+        return reverse('openedx.course_experience.course_reviews', args=[course_key])
diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
index 9b0a5133074..21abafd7fe1 100644
--- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
+++ b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
@@ -15,6 +15,7 @@ from django_comment_client.permissions import has_permission
 from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
 from openedx.core.djangolib.markup import HTML
 from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REVIEWS_TOOL_FLAG
+from openedx.features.course_experience.course_tools import CourseToolsPluginManager
 %>
 
 <%block name="content">
@@ -66,33 +67,26 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
                 ${HTML(outline_fragment.body_html())}
             </main>
             <aside class="course-sidebar layout-col layout-col-a">
-                <div class="section section-tools">
-                    <h3 class="hd-6">${_("Course Tools")}</h3>
-                    <ul class="list-unstyled">
-                        <li>
-                            <a href="${reverse('openedx.course_bookmarks.home', args=[course_key])}">
-                                <span class="icon fa fa-bookmark" aria-hidden="true"></span>
-                                ${_("Bookmarks")}
-                            </a>
-                        </li>
-                        % if SHOW_REVIEWS_TOOL_FLAG.is_enabled(course.id) and show_reviews_link:
-                            <li>
-                                <a href="${reverse('openedx.course_experience.course_reviews', args=[course.id])}">
-                                    <span class="icon fa fa-star" aria-hidden="true"></span>
-                                    ${_("Reviews")}
-                                </a>
-                            </li>
-                        % endif
-                        % if UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
-                            <li>
-                                <a href="${reverse('openedx.course_experience.course_updates', args=[course.id])}">
-                                    <span class="icon fa fa-newspaper-o" aria-hidden="true"></span>
-                                    ${_("Updates")}
-                                </a>
-                            </li>
-                        % endif
-                    </ul>
-                </div>
+                <%
+                course_tools = CourseToolsPluginManager.get_course_tools()
+                %>
+                % if course_tools:
+                    <div class="section section-tools">
+                        <h3 class="hd-6">${_("Course Tools")}</h3>
+                        <ul class="list-unstyled">
+                            % for course_tool in course_tools:
+                                % if course_tool.is_enabled(course_key):
+                                    <li>
+                                        <a href="${course_tool.url(course_key)}">
+                                            <span class="icon ${course_tool.icon_classes()}" aria-hidden="true"></span>
+                                            ${course_tool.title()}
+                                        </a>
+                                    </li>
+                                % endif
+                            % endfor
+                        </ul>
+                    </div>
+                % endif
                 <div class="section section-dates">
                     ${HTML(dates_fragment.body_html())}
                 </div>
diff --git a/setup.py b/setup.py
index 5a16348e426..6d9846c0b06 100644
--- a/setup.py
+++ b/setup.py
@@ -6,17 +6,15 @@ from setuptools import setup
 
 setup(
     name="Open edX",
-    version="0.6",
+    version="0.7",
     install_requires=["setuptools"],
     requires=[],
     # NOTE: These are not the names we should be installing.  This tree should
     # be reorganized to be a more conventional Python tree.
     packages=[
-        "openedx.core.djangoapps.course_groups",
-        "openedx.core.djangoapps.credit",
-        "openedx.core.djangoapps.user_api",
-        "lms",
         "cms",
+        "lms",
+        "openedx",
     ],
     entry_points={
         "openedx.course_tab": [
@@ -38,6 +36,11 @@ setup(
             "textbooks = lms.djangoapps.courseware.tabs:TextbookTabs",
             "wiki = lms.djangoapps.course_wiki.tab:WikiTab",
         ],
+        "openedx.course_tool": [
+            "course_bookmarks = openedx.features.course_bookmarks.plugins:CourseBookmarksTool",
+            "course_updates = openedx.features.course_experience.plugins:CourseUpdatesTool",
+            "course_reviews = openedx.features.course_experience.plugins:CourseReviewsTool",
+        ],
         "openedx.user_partition_scheme": [
             "random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme",
             "cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme",
-- 
GitLab