diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a279b5b..bbf5477 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,14 +54,31 @@ jobs: tag: ${{ steps.release.outputs.tag }} directory: './backend' - - name: Upload | Distribution Artifacts + - name: Upload | Backend Distribution Artifacts uses: actions/upload-artifact@v4 if: steps.release.outputs.released == 'true' with: - name: distribution-artifacts + name: backend-distribution-artifacts path: backend/dist if-no-files-found: error + - name: Build | Tutor Plugin + # The tutor plugin does not use python-semantic-release directly. + # Instead it shares the version determined by the backend's semantic + # release above, passed in via SETUPTOOLS_SCM_PRETEND_VERSION so that + # setuptools-scm picks it up at build time. + if: steps.release.outputs.released == 'true' + run: pip install build && SETUPTOOLS_SCM_PRETEND_VERSION=${{ steps.release.outputs.version }} python -m build + working-directory: './tutor' + + - name: Upload | Tutor Plugin Distribution Artifacts + uses: actions/upload-artifact@v4 + if: steps.release.outputs.released == 'true' + with: + name: tutor-distribution-artifacts + path: tutor/dist + if-no-files-found: error + outputs: released: ${{ steps.release.outputs.released || 'false' }} version: ${{ steps.release.outputs.version }} @@ -84,7 +101,7 @@ jobs: uses: actions/download-artifact@v4 id: artifact-download with: - name: distribution-artifacts + name: backend-distribution-artifacts path: backend/dist - name: Publish to PyPi @@ -94,6 +111,29 @@ jobs: user: __token__ password: ${{ secrets.PYPI_UPLOAD_TOKEN }} + publish_tutor_plugin_to_pypi: + runs-on: ubuntu-latest + needs: release + if: github.ref_name == 'main' && needs.release.outputs.released == 'true' + + permissions: + contents: read + id-token: write + + steps: + - name: Setup | Download Build Artifacts + uses: actions/download-artifact@v4 + with: + name: tutor-distribution-artifacts + path: tutor/dist + + - name: Publish to PyPi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: tutor/dist + user: __token__ + password: ${{ secrets.PYPI_UPLOAD_TOKEN }} + publish_to_npm: runs-on: ubuntu-latest needs: release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42f20c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,216 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/tutor/pyproject.toml b/tutor/pyproject.toml new file mode 100644 index 0000000..00e8877 --- /dev/null +++ b/tutor/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools", "setuptools-scm>8.1"] +build-backend = "setuptools.build_meta" + +[project] +name = "tutor-contrib-sample-plugin" +description = "Tutor plugin for the Open edX Sample Plugin" +requires-python = ">=3.11" +license = "Apache-2.0" +authors = [ + {name = "Open edX Project", email = "oscm@openedx.org"}, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] +keywords = ["tutor", "openedx", "plugin"] +dependencies = ["tutor>=17.0.0"] +dynamic = ["readme", "version"] + +[project.entry-points."tutor.plugin.v1"] +sample_plugin = "tutorsampleplugin.plugin" + +[project.urls] +Homepage = "https://github.com/openedx/sample-plugin" +Repository = "https://github.com/openedx/sample-plugin" + +[tool.setuptools.dynamic] +readme = {file = ["README.md"], content-type = "text/markdown"} + +[tool.setuptools.packages.find] +where = ["."] +include = ["tutorsampleplugin*"] + +[tool.setuptools_scm] +# The root for the git repo is one directory up. +root = ".." +version_scheme = "only-version" +local_scheme = "no-local-version" diff --git a/tutor/sample_plugin.py b/tutor/sample_plugin.py deleted file mode 100644 index 81acf15..0000000 --- a/tutor/sample_plugin.py +++ /dev/null @@ -1,16 +0,0 @@ -from tutormfe.hooks import PLUGIN_SLOTS - -PLUGIN_SLOTS.add_items([ - # Replace the course_list - ( - "learner-dashboard", - "custom_course_list", - """ - { - op: PLUGIN_OPERATIONS.Insert, - type: DIRECT_PLUGIN, - priority: 50, - RenderWidget: - }""" - ), -]) diff --git a/tutor/tutorsampleplugin/__init__.py b/tutor/tutorsampleplugin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tutor/tutorsampleplugin/plugin.py b/tutor/tutorsampleplugin/plugin.py new file mode 100644 index 0000000..3ed7689 --- /dev/null +++ b/tutor/tutorsampleplugin/plugin.py @@ -0,0 +1,95 @@ +""" +Tutor plugin for the Open edX Sample Plugin. + +Installs the backend Django app (openedx-sample-plugin from PyPI) into LMS/CMS +and configures the frontend MFE slot (from @openedx/sample-plugin on npm) in the +learner-dashboard. + +Requirements: + tutor>=17.0.0 + tutor-mfe (for frontend slot configuration) +""" + +from tutor import hooks + +try: + from tutormfe.hooks import PLUGIN_SLOTS + _tutormfe_available = True +except ImportError: + _tutormfe_available = False + +# --------------------------------------------------------------------------- +# Backend: Install the Django app plugin into LMS and CMS images +# --------------------------------------------------------------------------- +# The openedx-dockerfile-post-python-requirements patch runs after pip +# installs the base Open edX requirements. Plugins installed here are +# available in both LMS and CMS containers. +# --------------------------------------------------------------------------- + +hooks.Filters.ENV_PATCHES.add_item(( + "openedx-dockerfile-post-python-requirements", + "RUN pip install openedx-sample-plugin", +)) + +# --------------------------------------------------------------------------- +# Migrations: Run sample_plugin migrations on init +# --------------------------------------------------------------------------- + +hooks.Filters.CLI_DO_INIT_TASKS.add_item(( + "lms", + "./manage.py lms migrate sample_plugin", +)) + +# --------------------------------------------------------------------------- +# Frontend: Install npm package and configure the learner-dashboard slot +# --------------------------------------------------------------------------- +# Only runs when tutor-mfe is installed, so the plugin degrades gracefully +# if someone uses this plugin without the MFE plugin. +# --------------------------------------------------------------------------- + +if _tutormfe_available: + # Step 1: Install the npm package into all MFE images. + # Ideally this would use mfe-dockerfile-post-npm-install-learner-dashboard + # to scope installation to only the MFE that needs it, but env.config.jsx + # is a single shared file rendered for all MFEs. The buildtime import below + # must resolve in every MFE's node_modules, so we install it globally. + # The plugin slot config is still scoped to learner-dashboard at runtime. + hooks.Filters.ENV_PATCHES.add_item(( + "mfe-dockerfile-post-npm-install", + "RUN npm install @openedx/sample-plugin", + )) + + # Step 2: Import the CourseList component in the MFE env config so it is + # in scope when the plugin slot configuration is evaluated at runtime. + # The mfe-env-config-buildtime-imports patch injects import statements + # into the generated env.config.jsx file. + hooks.Filters.ENV_PATCHES.add_item(( + "mfe-env-config-buildtime-imports", + "import { CourseList } from '@openedx/sample-plugin';", + )) + + # Step 3: Configure the course list plugin slot. + # - Hide the default CourseList that ships with the learner-dashboard. + # - Insert our custom CourseList that adds archive/unarchive functionality. + # + # Slot ID: org.openedx.frontend.learner_dashboard.course_list.v1 + # Props passed by the slot: courseListData (visibleList, numPages, + # setPageNumber, filterOptions, showFilters) + PLUGIN_SLOTS.add_item(( + "learner-dashboard", + "org.openedx.frontend.learner_dashboard.course_list.v1", + """ + { + op: PLUGIN_OPERATIONS.Hide, + widgetId: 'default_contents', + }, + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'sample_plugin_course_list', + type: DIRECT_PLUGIN, + priority: 50, + RenderWidget: CourseList, + }, + }""", + ))