diff --git a/src/ol_openedx_chat/BUILD b/src/ol_openedx_chat/BUILD new file mode 100644 index 00000000..d4beda50 --- /dev/null +++ b/src/ol_openedx_chat/BUILD @@ -0,0 +1,41 @@ +python_sources( + name="ol_openedx_chat", + dependencies=[ + "src/ol_openedx_chat/settings:ol_chat_settings", + ], +) + +python_distribution( + name="ol_openedx_chat_package", + dependencies=[ + ":ol_openedx_chat", + "src/ol_openedx_chat/static/js:ol_chat_js", + "src/ol_openedx_chat/static/css:ol_chat_css", + "src/ol_openedx_chat/static/html:ol_chat_html", + ], + provides=setup_py( + name="ol-openedx-chat", + version="0.1.0", + description="An Open edX plugin to add Open Learning AI chat aside to xBlocks", + license="BSD-3-Clause", + author="MIT Office of Digital Learning", + include_package_data=True, + zip_safe=False, + keywords="Python edx", + entry_points={ + 'xblock_asides.v1': [ + 'ol_openedx_chat = ol_openedx_chat.block:OLChatAside', + ], + "lms.djangoapp": [ + "ol_openedx_chat = ol_openedx_chat.apps:OLOpenedxChatConfig" + ], + "cms.djangoapp": [ + "ol_openedx_chat = ol_openedx_chat.apps:OLOpenedxChatConfig" + ], + }, + ), +) + +python_tests( + name="tests", +) diff --git a/src/ol_openedx_chat/CHANGELOG.rst b/src/ol_openedx_chat/CHANGELOG.rst new file mode 100644 index 00000000..84b3c4d9 --- /dev/null +++ b/src/ol_openedx_chat/CHANGELOG.rst @@ -0,0 +1,11 @@ +Change Log +---------- + +.. + All enhancements and patches to ol_openedx_chat will be documented + in this file. It adheres to the structure of https://keepachangelog.com/ , + but in reStructuredText instead of Markdown (for ease of incorporation into + Sphinx documentation and the PyPI description). + + This project adheres to Semantic Versioning (https://semver.org/). +.. There should always be an "Unreleased" section for changes pending release. diff --git a/src/ol_openedx_chat/LICENSE.txt b/src/ol_openedx_chat/LICENSE.txt new file mode 100644 index 00000000..83284fb7 --- /dev/null +++ b/src/ol_openedx_chat/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright (C) 2022 MIT Open Learning + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/ol_openedx_chat/README.rst b/src/ol_openedx_chat/README.rst new file mode 100644 index 00000000..70ee2428 --- /dev/null +++ b/src/ol_openedx_chat/README.rst @@ -0,0 +1,31 @@ +ol-openedx-chat +############### + +An xBlock aside to add MIT Open Learning chat into xBlocks + + +Purpose +******* + +MIT's AI chatbot for Open edX + +Getting Started with Development +******************************** + + +Deploying +********* + +Getting Help +************ + +Documentation +============= + +License +******* + +The code in this repository is licensed under the AGPL 3.0 unless +otherwise noted. + +Please see `LICENSE.txt `_ for details. diff --git a/src/ol_openedx_chat/__init__.py b/src/ol_openedx_chat/__init__.py new file mode 100644 index 00000000..ca6ebba2 --- /dev/null +++ b/src/ol_openedx_chat/__init__.py @@ -0,0 +1,7 @@ +""" +MIT's AI chatbot for Open edX +""" + +__version__ = "0.1.0" + +default_app_config = "ol_openedx_chat.apps.OLOpenedxChatConfig" diff --git a/src/ol_openedx_chat/apps.py b/src/ol_openedx_chat/apps.py new file mode 100644 index 00000000..96fc2f31 --- /dev/null +++ b/src/ol_openedx_chat/apps.py @@ -0,0 +1,28 @@ +""" +ol_openedx_chat Django application initialization. +""" + +from django.apps import AppConfig +from edx_django_utils.plugins import PluginSettings +from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType + + +class OLOpenedxChatConfig(AppConfig): + """ + Configuration for the ol_openedx_chat Django application. + """ + + name = "ol_openedx_chat" + + plugin_app = { + PluginSettings.CONFIG: { + ProjectType.LMS: { + SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.common"}, + }, + ProjectType.CMS: { + SettingsType.COMMON: { + PluginSettings.RELATIVE_PATH: "settings.cms_settings" + }, + }, + }, + } diff --git a/src/ol_openedx_chat/block.py b/src/ol_openedx_chat/block.py new file mode 100644 index 00000000..a6f56f04 --- /dev/null +++ b/src/ol_openedx_chat/block.py @@ -0,0 +1,79 @@ +import pkg_resources +from django.template import Context, Template +from web_fragments.fragment import Fragment +from xblock.core import XBlockAside + +BLOCK_PROBLEM_CATEGORY = "problem" +MULTIPLE_CHOICE_TYPE = "multiplechoiceresponse" + + +def get_resource_bytes(path): + """ + Helper method to get the unicode contents of a resource in this repo. + + Args: + path (str): The path of the resource + + Returns: + unicode: The unicode contents of the resource at the given path + """ # noqa: D401 + resource_contents = pkg_resources.resource_string(__name__, path) + return resource_contents.decode("utf-8") + + +def render_template(template_path, context=None): + """ + Evaluate a template by resource path, applying the provided context. + """ + context = context or {} + template_str = get_resource_bytes(template_path) + template = Template(template_str) + return template.render(Context(context)) + + +class OLChatAside(XBlockAside): + """ + XBlock aside that enables OL AI Chat functionality for an XBlock + """ + + @XBlockAside.aside_for("student_view") + def student_view_aside(self, block, context=None): # noqa: ARG002 + """ + Renders the aside contents for the student view + """ # noqa: D401 + fragment = Fragment("") + fragment.add_content(render_template("static/html/student_view.html")) + return fragment + + @XBlockAside.aside_for("author_view") + def author_view_aside(self, block, context=None): # noqa: ARG002 + """ + Renders the aside contents for the author view + """ # noqa: D401 + fragment = Fragment("") + fragment.add_content(render_template("static/html/studio_view.html")) + return fragment + + @classmethod + def should_apply_to_block(cls, block): + """ + Overrides base XBlockAside implementation. Indicates whether or not this aside + should apply to a given block. + + Due to the different ways that the Studio and LMS runtimes construct XBlock + instances, the problem type of the given block needs to be retrieved in + different ways. + """ # noqa: D401 + if getattr(block, "category", None) != BLOCK_PROBLEM_CATEGORY: + return False + block_problem_types = None + # LMS passes in the block instance with `problem_types` as a property of + # `descriptor` + if hasattr(block, "descriptor"): + block_problem_types = getattr(block.descriptor, "problem_types", None) + # Studio passes in the block instance with `problem_types` as a top-level property # noqa: E501 + elif hasattr(block, "problem_types"): + block_problem_types = block.problem_types + # We only want this aside to apply to the block if the problem is multiple + # choice AND there are not multiple problem types. + return block_problem_types == {MULTIPLE_CHOICE_TYPE} diff --git a/src/ol_openedx_chat/settings/BUILD b/src/ol_openedx_chat/settings/BUILD new file mode 100644 index 00000000..4ce6d987 --- /dev/null +++ b/src/ol_openedx_chat/settings/BUILD @@ -0,0 +1 @@ +python_sources(name="ol_chat_settings") diff --git a/src/ol_openedx_chat/settings/cms_settings.py b/src/ol_openedx_chat/settings/cms_settings.py new file mode 100644 index 00000000..c268b6eb --- /dev/null +++ b/src/ol_openedx_chat/settings/cms_settings.py @@ -0,0 +1,11 @@ +# noqa: INP001 +"""Settings to provide to edX""" + + +def plugin_settings(settings): # noqa: ARG001 + """ + Populate CMS settings + """ + + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/src/ol_openedx_chat/settings/common.py b/src/ol_openedx_chat/settings/common.py new file mode 100644 index 00000000..cc7f6433 --- /dev/null +++ b/src/ol_openedx_chat/settings/common.py @@ -0,0 +1,11 @@ +# noqa: INP001 +"""Settings to provide to edX""" + + +def plugin_settings(settings): # noqa: ARG001 + """ + Populate settings + """ + + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/src/ol_openedx_chat/setup.cfg b/src/ol_openedx_chat/setup.cfg new file mode 100644 index 00000000..d7825996 --- /dev/null +++ b/src/ol_openedx_chat/setup.cfg @@ -0,0 +1,10 @@ +[isort] +include_trailing_comma = True +indent = ' ' +line_length = 120 +multi_line_output = 3 +skip= + migrations + +[wheel] +universal = 1 diff --git a/src/ol_openedx_chat/static/css/BUILD b/src/ol_openedx_chat/static/css/BUILD new file mode 100644 index 00000000..feed9288 --- /dev/null +++ b/src/ol_openedx_chat/static/css/BUILD @@ -0,0 +1,4 @@ +resources( + name="ol_chat_css", + sources=["*.css"], +) diff --git a/src/ol_openedx_chat/static/html/BUILD b/src/ol_openedx_chat/static/html/BUILD new file mode 100644 index 00000000..7b0ed723 --- /dev/null +++ b/src/ol_openedx_chat/static/html/BUILD @@ -0,0 +1,4 @@ +resources( + name="ol_chat_html", + sources=["*.html"], +) diff --git a/src/ol_openedx_chat/static/html/student_view.html b/src/ol_openedx_chat/static/html/student_view.html new file mode 100644 index 00000000..7509f4f0 --- /dev/null +++ b/src/ol_openedx_chat/static/html/student_view.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/ol_openedx_chat/static/html/studio_view.html b/src/ol_openedx_chat/static/html/studio_view.html new file mode 100644 index 00000000..a5265085 --- /dev/null +++ b/src/ol_openedx_chat/static/html/studio_view.html @@ -0,0 +1,25 @@ +
+ +
+ + +
+ +
+ + +
+ +
diff --git a/src/ol_openedx_chat/static/js/BUILD b/src/ol_openedx_chat/static/js/BUILD new file mode 100644 index 00000000..3afebf49 --- /dev/null +++ b/src/ol_openedx_chat/static/js/BUILD @@ -0,0 +1,4 @@ +resources( + name="ol_chat_js", + sources=["src_js/*.js","lib/*.js"], +) diff --git a/src/ol_openedx_chat/tests/__init__.py b/src/ol_openedx_chat/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ol_openedx_chat/tests/conftest.py b/src/ol_openedx_chat/tests/conftest.py new file mode 100644 index 00000000..f0b931e4 --- /dev/null +++ b/src/ol_openedx_chat/tests/conftest.py @@ -0,0 +1,41 @@ +"""Pytest config""" + +import json +import logging +from pathlib import Path + +import pytest + +BASE_DIR = Path(__file__).parent.absolute() + + +def pytest_addoption(parser): + """Pytest hook that adds command line options""" + parser.addoption( + "--disable-logging", + action="store_true", + default=False, + help="Disable all logging during test run", + ) + parser.addoption( + "--error-log-only", + action="store_true", + default=False, + help="Disable all logging output below 'error' level during test run", + ) + + +def pytest_configure(config): + """Pytest hook that runs after command line options have been parsed""" + if config.getoption("--disable-logging"): + logging.disable(logging.CRITICAL) + elif config.getoption("--error-log-only"): + logging.disable(logging.WARNING) + + +@pytest.fixture() +def example_event(request): # noqa: PT004 + """An example real event captured previously""" # noqa: D401 + with Path.open(BASE_DIR / ".." / "test_data" / "example_event.json") as f: + request.cls.example_event = json.load(f) + yield diff --git a/src/ol_openedx_chat/urls.py b/src/ol_openedx_chat/urls.py new file mode 100644 index 00000000..f39ce321 --- /dev/null +++ b/src/ol_openedx_chat/urls.py @@ -0,0 +1,8 @@ +""" +URLs for ol_openedx_chat. +""" + +urlpatterns = [ + # Fill in URL patterns and views here. + # re_path(r'', TemplateView.as_view(template_name="ol_openedx_chat/base.html")), # noqa: ERA001, E501 +]