From 0d65c574164a08bcf131875befa1497e882de1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Tue, 21 Sep 2021 12:06:34 +0200 Subject: [PATCH] :sunrise: --- .github/workflows/sync.yml | 17 ++++ .gitignore | 7 ++ .gitlab-ci.yml | 10 +++ MANIFEST.in | 2 + README.rst | 82 +++++++++++++++++++ contrib/edx-platform/README.rst | 4 + contrib/edx-platform/richie/__init__.py | 0 contrib/edx-platform/richie/apps.py | 38 +++++++++ .../edx-platform/richie/settings/__init__.py | 0 contrib/edx-platform/richie/settings/cms.py | 7 ++ contrib/edx-platform/richie/settings/lms.py | 3 + contrib/edx-platform/richie/signals.py | 12 +++ contrib/edx-platform/richie/sync.py | 71 ++++++++++++++++ contrib/edx-platform/richie/urls.py | 7 ++ contrib/edx-platform/richie/views.py | 15 ++++ contrib/edx-platform/setup.py | 49 +++++++++++ setup.py | 61 ++++++++++++++ tutorrichie/__about__.py | 1 + tutorrichie/__init__.py | 0 tutorrichie/patches/.gitignore | 0 tutorrichie/patches/caddyfile | 4 + tutorrichie/patches/k8s-deployments | 32 ++++++++ tutorrichie/patches/k8s-jobs | 25 ++++++ tutorrichie/patches/k8s-services | 12 +++ tutorrichie/patches/kustomization | 4 + .../patches/kustomization-configmapgenerator | 3 + .../patches/local-docker-compose-dev-services | 6 ++ .../local-docker-compose-jobs-services | 21 +++++ .../patches/local-docker-compose-services | 9 ++ tutorrichie/patches/nginx-extra | 19 +++++ .../patches/openedx-cms-development-settings | 6 ++ .../patches/openedx-cms-production-settings | 11 +++ tutorrichie/patches/openedx-common-settings | 2 + .../patches/openedx-development-settings | 6 ++ ...penedx-dockerfile-post-python-requirements | 2 + .../patches/openedx-lms-development-settings | 2 + .../patches/openedx-lms-production-settings | 7 ++ tutorrichie/plugin.py | 41 ++++++++++ tutorrichie/templates/richie/apps/.gitignore | 0 .../templates/richie/apps/env.d/development | 3 + .../templates/richie/apps/env.d/production | 17 ++++ .../templates/richie/apps/settings/tutor.py | 71 ++++++++++++++++ tutorrichie/templates/richie/build/.gitignore | 0 .../templates/richie/build/richie/Dockerfile | 80 ++++++++++++++++++ tutorrichie/templates/richie/hooks/.gitignore | 0 tutorrichie/templates/richie/hooks/mysql/init | 2 + .../richie/hooks/richie-openedx/init | 2 + .../templates/richie/hooks/richie/init | 11 +++ 48 files changed, 784 insertions(+) create mode 100644 .github/workflows/sync.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 contrib/edx-platform/README.rst create mode 100644 contrib/edx-platform/richie/__init__.py create mode 100644 contrib/edx-platform/richie/apps.py create mode 100644 contrib/edx-platform/richie/settings/__init__.py create mode 100644 contrib/edx-platform/richie/settings/cms.py create mode 100644 contrib/edx-platform/richie/settings/lms.py create mode 100644 contrib/edx-platform/richie/signals.py create mode 100644 contrib/edx-platform/richie/sync.py create mode 100644 contrib/edx-platform/richie/urls.py create mode 100644 contrib/edx-platform/richie/views.py create mode 100644 contrib/edx-platform/setup.py create mode 100644 setup.py create mode 100644 tutorrichie/__about__.py create mode 100644 tutorrichie/__init__.py create mode 100644 tutorrichie/patches/.gitignore create mode 100644 tutorrichie/patches/caddyfile create mode 100644 tutorrichie/patches/k8s-deployments create mode 100644 tutorrichie/patches/k8s-jobs create mode 100644 tutorrichie/patches/k8s-services create mode 100644 tutorrichie/patches/kustomization create mode 100644 tutorrichie/patches/kustomization-configmapgenerator create mode 100644 tutorrichie/patches/local-docker-compose-dev-services create mode 100644 tutorrichie/patches/local-docker-compose-jobs-services create mode 100644 tutorrichie/patches/local-docker-compose-services create mode 100644 tutorrichie/patches/nginx-extra create mode 100644 tutorrichie/patches/openedx-cms-development-settings create mode 100644 tutorrichie/patches/openedx-cms-production-settings create mode 100644 tutorrichie/patches/openedx-common-settings create mode 100644 tutorrichie/patches/openedx-development-settings create mode 100644 tutorrichie/patches/openedx-dockerfile-post-python-requirements create mode 100644 tutorrichie/patches/openedx-lms-development-settings create mode 100644 tutorrichie/patches/openedx-lms-production-settings create mode 100644 tutorrichie/plugin.py create mode 100644 tutorrichie/templates/richie/apps/.gitignore create mode 100644 tutorrichie/templates/richie/apps/env.d/development create mode 100644 tutorrichie/templates/richie/apps/env.d/production create mode 100644 tutorrichie/templates/richie/apps/settings/tutor.py create mode 100644 tutorrichie/templates/richie/build/.gitignore create mode 100644 tutorrichie/templates/richie/build/richie/Dockerfile create mode 100644 tutorrichie/templates/richie/hooks/.gitignore create mode 100644 tutorrichie/templates/richie/hooks/mysql/init create mode 100644 tutorrichie/templates/richie/hooks/richie-openedx/init create mode 100644 tutorrichie/templates/richie/hooks/richie/init diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 0000000..2eb53ff --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,17 @@ +name: Sync with private repo + +on: + push: + branches: [ master ] + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Add remote + run: git remote add overhangio https://${{ secrets.GIT_USERNAME }}:${{ secrets.GIT_PASSWORD }}@git.overhang.io/core/tutor-richie.git + - name: Push + run: git push overhangio master diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6a874f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.*.swp +!.gitignore +TODO +__pycache__ +*.egg-info/ +/build/ +/dist/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7053457 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,10 @@ +variables: + TUTOR_PLUGIN: richie + TUTOR_IMAGES: richie + TUTOR_PYPI_PACKAGE: tutor-richie + OPENEDX_RELEASE: lilac + GITHUB_REPO: overhangio/tutor-richie + +include: + - project: 'community/tutor-ci' + file: 'plugin-gitlab-ci.yml' diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c60af67 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include tutorrichie/patches * +recursive-include tutorrichie/templates * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..42fcff6 --- /dev/null +++ b/README.rst @@ -0,0 +1,82 @@ +Richie plugin for `Tutor `__ +============================================================ + +This is a plugin to integrate `Richie `__, the learning portal CMS, with `Open edX `__. The integration takes the form of a `Tutor `__ plugin. + +Installation +------------ + +:: + + pip install tutor-richie + tutor plugins enable richie + +Running the Richie plugin will require that you rebuild the "openedx" Docker image:: + + tutor config save + tutor images build openedx + +This step is necessary to install the Richie connector app in edx-platform. + +Then, the platform can be launched as usual with:: + + tutor local quickstart + +You will be able to access your course catalog at http(s)://courses.LMS_HOST. In development, this url will be http://courses.local.overhang.io. + +Gettting started +---------------- + +Once your Richie platform is up and running, you will quickly realize that your learning portal is empty. This is because you should first create the corresponding courses and organizations from inside Richie. To do so, start by creating a super user:: + + tutor local run richie ./sandbox/manage.py createsuperuser + +You can then use the credentials you just created at http(s)://yourrichiehost/admin. In development, this is http://courses.local.overhang.io/admin. + +Then, refer to the official `Richie documentation `__ to learn how to create courses and organizations. + +You may also want to fill your learning portal with a demo site -- but be careful not to run this command in production, as it will be difficult to get rid of the demo site afterwards:: + + # WARNING: do not attempt this in production! + tutor local run richie ./sandbox/manage.py create_demo_site --force + +Configuration +------------- + +This Tutor plugin comes with a few configuration settings: + +- ``RICHIE_RELEASE_VERSION`` (default: ``"v2.8.2"``) +- ``RICHIE_HOST`` (default: ``"courses.{{ LMS_HOST }}"``) +- ``RICHIE_MYSQL_DATABASE`` (default: ``"richie"``) +- ``RICHIE_MYSQL_USERNAME`` (default: ``"richie"``) +- ``RICHIE_ELASTICSEARCH_INDICES_PREFIX`` (default: ``"richie"``) + +These defaults should be enough for most users. To modify any one of them, run:: + + tutor config save --set RICHIE_SETTING_NAME=myvalue + +For instance, to customize the domain name at which Richie will run:: + + tutor config save --set "RICHIE_HOST=mysubdomain.{{ LMS_HOST }}" + +Development +----------- + +Bind-mount volume:: + + tutor dev bindmount richie /app/richie + +Then, run a development server:: + + tutor dev runserver --volume=/app/richie richie + +The Richie development server will be available at http://courses.local.overhang.io:8003. + +License +------- + +This software is licensed under the terms of the `AGPLv3 `__. It was developed and is being actively maintained thanks to the sponsorship of `France Université Numérique `__. + +.. image:: https://www.fun-mooc.fr/static/richie/images/logo.png + :alt: France Université Numérique + :target: https://fun-mooc.fr diff --git a/contrib/edx-platform/README.rst b/contrib/edx-platform/README.rst new file mode 100644 index 0000000..f320c4d --- /dev/null +++ b/contrib/edx-platform/README.rst @@ -0,0 +1,4 @@ +Richie connector plugin app for Open edX +======================================== + +This is an `Open edX plugin `__ to facilitate the integration of a `Richie `__ learning portal. Its main feature is automatically syncing course properties when they are updated in the Open edX studio. diff --git a/contrib/edx-platform/richie/__init__.py b/contrib/edx-platform/richie/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/edx-platform/richie/apps.py b/contrib/edx-platform/richie/apps.py new file mode 100644 index 0000000..91ebb72 --- /dev/null +++ b/contrib/edx-platform/richie/apps.py @@ -0,0 +1,38 @@ +from django.apps import AppConfig +from edx_django_utils.plugins.constants import PluginSettings, PluginURLs +from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType + + +class RichieAppConfig(AppConfig): + name = "richie" + verbose_name = "Richie course catalog connector" + + # Open edX plugin docs: https://github.com/edx/edx-django-utils/blob/master/edx_django_utils/plugins/README.rst + plugin_app = { + PluginURLs.CONFIG: { + ProjectType.LMS: { + PluginURLs.NAMESPACE: "richie", + PluginURLs.REGEX: r"^richie/", + PluginURLs.RELATIVE_PATH: "urls", + } + }, + PluginSettings.CONFIG: { + ProjectType.LMS: { + SettingsType.COMMON: { + PluginSettings.RELATIVE_PATH: "settings.lms", + }, + }, + ProjectType.CMS: { + SettingsType.COMMON: { + PluginSettings.RELATIVE_PATH: "settings.cms", + }, + }, + }, + } + + def ready(self): + """ + Connect signal handlers. + """ + # pylint: disable=unused-import + from . import signals diff --git a/contrib/edx-platform/richie/settings/__init__.py b/contrib/edx-platform/richie/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/edx-platform/richie/settings/cms.py b/contrib/edx-platform/richie/settings/cms.py new file mode 100644 index 0000000..cbe2d51 --- /dev/null +++ b/contrib/edx-platform/richie/settings/cms.py @@ -0,0 +1,7 @@ +def plugin_settings(settings): + """Common settings for Richie catalog connector""" + settings.RICHIE_COURSE_HOOK = { + "secret": "richiesecret", + "url": "http://richie:8000/api/v1.0/course-runs-sync/", + "timeout": 3, + } diff --git a/contrib/edx-platform/richie/settings/lms.py b/contrib/edx-platform/richie/settings/lms.py new file mode 100644 index 0000000..4ffc029 --- /dev/null +++ b/contrib/edx-platform/richie/settings/lms.py @@ -0,0 +1,3 @@ +def plugin_settings(settings): + """Common settings for Richie catalog connector""" + settings.RICHIE_ROOT_URL = "http://myrichie" diff --git a/contrib/edx-platform/richie/signals.py b/contrib/edx-platform/richie/signals.py new file mode 100644 index 0000000..fe7403c --- /dev/null +++ b/contrib/edx-platform/richie/signals.py @@ -0,0 +1,12 @@ +from django.dispatch import receiver +from xmodule.modulestore.django import SignalHandler + +from .sync import sync_course_from_key + + +@receiver(SignalHandler.course_published, dispatch_uid="update_course_on_publish") +def update_course_on_publish(sender, course_key, **kwargs): + """ + Synchronize course properties with Richie catalog. + """ + sync_course_from_key(course_key) diff --git a/contrib/edx-platform/richie/sync.py b/contrib/edx-platform/richie/sync.py new file mode 100644 index 0000000..fbf3699 --- /dev/null +++ b/contrib/edx-platform/richie/sync.py @@ -0,0 +1,71 @@ +import hashlib +import hmac +import json +import logging + + +from django.conf import settings +import requests +import requests.exceptions +from xmodule.modulestore.django import modulestore + +logger = logging.getLogger(__name__) + + +def sync_all_courses(): + """ + Synchronize all courses with Richie. + """ + for course in modulestore().get_courses(): + sync_course(course) + + +def sync_course_from_key(course_key): + return sync_course(modulestore().get_course(course_key)) + + +def sync_course(course): + """ + Synchronize an Open edX course with a Richie instance. + + Note that only the course settings are synchronized, and not the actual + course contents or description. This function always succeeds, even when the + request fails. + """ + enrollment_start = course.enrollment_start and course.enrollment_start.isoformat() + enrollment_end = course.enrollment_end and course.enrollment_end.isoformat() + data = { + "resource_link": "{}/courses/{}/course".format( + settings.LMS_ROOT_URL, course.id + ), + "start": course.start and course.start.isoformat(), + "end": course.end and course.end.isoformat(), + "enrollment_start": enrollment_start, + "enrollment_end": enrollment_end, + "languages": [course.language or settings.LANGUAGE_CODE], + } + + signature = hmac.new( + settings.RICHIE_COURSE_HOOK["secret"].encode("utf-8"), + msg=json.dumps(data).encode("utf-8"), + digestmod=hashlib.sha256, + ).hexdigest() + + try: + response = requests.post( + settings.RICHIE_COURSE_HOOK["url"], + json=data, + headers={"Authorization": "SIG-HMAC-SHA256 {:s}".format(signature)}, + timeout=settings.RICHIE_COURSE_HOOK["timeout"], + ) + except requests.exceptions.Timeout: + logger.error( + f"Could not synchronize course {course.id} with Richie. Response timeout" + ) + return + if response.status_code >= 400: + logger.error( + f"Could not synchronize course {course.id} with Richie. Response: {response.content.decode()}" + ) + else: + logger.info(f"Successfuly synchronized course {course.id} with Richie") diff --git a/contrib/edx-platform/richie/urls.py b/contrib/edx-platform/richie/urls.py new file mode 100644 index 0000000..009d2fd --- /dev/null +++ b/contrib/edx-platform/richie/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.redirect_to_richie, name="redirect_to_richie"), +] diff --git a/contrib/edx-platform/richie/views.py b/contrib/edx-platform/richie/views.py new file mode 100644 index 0000000..be1c8fe --- /dev/null +++ b/contrib/edx-platform/richie/views.py @@ -0,0 +1,15 @@ +from urllib.parse import urljoin + + +from django.conf import settings +from django.shortcuts import redirect + + +def redirect_to_richie(request, subpath): + """ + Redirect to Richie catalog. + + This view is used after login from Richie, with a "?next=richie/en" + parameter. + """ + return redirect(urljoin(settings.RICHIE_ROOT_URL, subpath)) diff --git a/contrib/edx-platform/setup.py b/contrib/edx-platform/setup.py new file mode 100644 index 0000000..4041832 --- /dev/null +++ b/contrib/edx-platform/setup.py @@ -0,0 +1,49 @@ +import io +import os +from setuptools import setup, find_packages + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def load_readme(): + with io.open(os.path.join(HERE, "README.rst"), "rt", encoding="utf8") as f: + return f.read() + + +setup( + name="richie-edx-platform", + version="0.0.1", + url="https://github.com/overhangio/tutor-richie/", + project_urls={ + "Code": "https://github.com/overhangio/tutor-richie/tree/master/contrib/edx-platform", + "Issue tracker": "https://github.com/overhangio/tutor-richie/issues", + }, + license="AGPLv3", + author="Overhang.IO", + description="Open edX plugin app for integration with a Richie catalog", + long_description=load_readme(), + packages=find_packages(exclude=["tests*"]), + include_package_data=True, + entry_points={ + "lms.djangoapp": [ + "richie = richie.apps:RichieAppConfig", + ], + "cms.djangoapp": [ + "richie = richie.apps:RichieAppConfig", + ], + }, + python_requires=">=3.5", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], +) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..24275a1 --- /dev/null +++ b/setup.py @@ -0,0 +1,61 @@ +import io +import os +from setuptools import setup, find_packages + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def load_readme(): + with io.open(os.path.join(HERE, "README.rst"), "rt", encoding="utf8") as f: + return f.read() + + +def load_about(): + about = {} + with io.open( + os.path.join(HERE, "tutorrichie", "__about__.py"), + "rt", + encoding="utf-8", + ) as f: + exec(f.read(), about) # pylint: disable=exec-used + return about + + +ABOUT = load_about() + + +setup( + name="tutor-richie", + version=ABOUT["__version__"], + url="https://github.com/overhangio/tutor-richie", + project_urls={ + "Code": "https://github.com/overhangio/tutor-richie", + "Issue tracker": "https://github.com/overhangio/tutor-richie/issues", + }, + license="AGPLv3", + author="Overhang.IO", + description="richie plugin for Tutor", + long_description=load_readme(), + packages=find_packages(exclude=["tests*", "contrib*"]), + include_package_data=True, + python_requires=">=3.5", + install_requires=["tutor>=12.0.0,<13.0.0"], + entry_points={ + "tutor.plugin.v0": [ + "richie = tutorrichie.plugin" + ] + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], +) diff --git a/tutorrichie/__about__.py b/tutorrichie/__about__.py new file mode 100644 index 0000000..0e21ac9 --- /dev/null +++ b/tutorrichie/__about__.py @@ -0,0 +1 @@ +__version__ = "12.0.0" diff --git a/tutorrichie/__init__.py b/tutorrichie/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tutorrichie/patches/.gitignore b/tutorrichie/patches/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tutorrichie/patches/caddyfile b/tutorrichie/patches/caddyfile new file mode 100644 index 0000000..bff7ad4 --- /dev/null +++ b/tutorrichie/patches/caddyfile @@ -0,0 +1,4 @@ +# Richie +{{ RICHIE_HOST }}{% if not ENABLE_HTTPS %}:80{% endif %} { + reverse_proxy nginx:80 +} diff --git a/tutorrichie/patches/k8s-deployments b/tutorrichie/patches/k8s-deployments new file mode 100644 index 0000000..78183a7 --- /dev/null +++ b/tutorrichie/patches/k8s-deployments @@ -0,0 +1,32 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: richie + labels: + app.kubernetes.io/name: richie +spec: + selector: + matchLabels: + app.kubernetes.io/name: richie + template: + metadata: + labels: + app.kubernetes.io/name: richie + spec: + containers: + - name: richie + image: {{ RICHIE_DOCKER_IMAGE }} + ports: + - containerPort: 8000 + volumeMounts: + - mountPath: /app/richie/sandbox/tutor.py + name: settings + subPath: tutor.py + envFrom: + - secretRef: + name: richie-env + volumes: + - name: settings + configMap: + name: richie-settings diff --git a/tutorrichie/patches/k8s-jobs b/tutorrichie/patches/k8s-jobs new file mode 100644 index 0000000..4fb0c38 --- /dev/null +++ b/tutorrichie/patches/k8s-jobs @@ -0,0 +1,25 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: richie-job + labels: + app.kubernetes.io/component: job +spec: + template: + spec: + restartPolicy: Never + containers: + - name: richie + image: {{ RICHIE_DOCKER_IMAGE }} + volumeMounts: + - mountPath: /app/richie/sandbox/tutor.py + name: settings + subPath: tutor.py + envFrom: + - secretRef: + name: richie-env + volumes: + - name: settings + configMap: + name: richie-settings diff --git a/tutorrichie/patches/k8s-services b/tutorrichie/patches/k8s-services new file mode 100644 index 0000000..c3fc24b --- /dev/null +++ b/tutorrichie/patches/k8s-services @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: richie +spec: + type: NodePort + ports: + - port: 8000 + protocol: TCP + selector: + app.kubernetes.io/name: richie diff --git a/tutorrichie/patches/kustomization b/tutorrichie/patches/kustomization new file mode 100644 index 0000000..63beaf4 --- /dev/null +++ b/tutorrichie/patches/kustomization @@ -0,0 +1,4 @@ +secretGenerator: +- name: richie-env + files: + - plugins/richie/apps/env.d/production diff --git a/tutorrichie/patches/kustomization-configmapgenerator b/tutorrichie/patches/kustomization-configmapgenerator new file mode 100644 index 0000000..6bdf45f --- /dev/null +++ b/tutorrichie/patches/kustomization-configmapgenerator @@ -0,0 +1,3 @@ +- name: richie-settings + files: + - plugins/richie/apps/settings/tutor.py diff --git a/tutorrichie/patches/local-docker-compose-dev-services b/tutorrichie/patches/local-docker-compose-dev-services new file mode 100644 index 0000000..fbad54b --- /dev/null +++ b/tutorrichie/patches/local-docker-compose-dev-services @@ -0,0 +1,6 @@ +richie: + command: ./sandbox/manage.py runserver 0.0.0.0:8003 + env_file: + - ../plugins/richie/apps/env.d/development + ports: + - "8003:8003" diff --git a/tutorrichie/patches/local-docker-compose-jobs-services b/tutorrichie/patches/local-docker-compose-jobs-services new file mode 100644 index 0000000..ae3287c --- /dev/null +++ b/tutorrichie/patches/local-docker-compose-jobs-services @@ -0,0 +1,21 @@ +richie-job: + image: {{ RICHIE_DOCKER_IMAGE }} + depends_on: {{ [("elasticsearch", RUN_ELASTICSEARCH), ("mysql", RUN_MYSQL)]|list_if }} + # Run as root to fix media permissions + user: root + env_file: + - ../plugins/richie/apps/env.d/production + volumes: + - ../plugins/richie/apps/settings/tutor.py:/app/richie/sandbox/tutor.py:ro + - ../../data/richie/media:/data/media +richie-openedx-job: + image: {{ DOCKER_IMAGE_OPENEDX }} + environment: + SERVICE_VARIANT: cms + SETTINGS: ${TUTOR_EDX_PLATFORM_SETTINGS:-tutor.production} + volumes: + - ../apps/openedx/settings/lms/:/openedx/edx-platform/lms/envs/tutor/:ro + - ../apps/openedx/settings/cms/:/openedx/edx-platform/cms/envs/tutor/:ro + - ../apps/openedx/config/:/openedx/config/:ro + depends_on: + - richie diff --git a/tutorrichie/patches/local-docker-compose-services b/tutorrichie/patches/local-docker-compose-services new file mode 100644 index 0000000..f577d75 --- /dev/null +++ b/tutorrichie/patches/local-docker-compose-services @@ -0,0 +1,9 @@ +richie: + image: {{ RICHIE_DOCKER_IMAGE }} + restart: unless-stopped + env_file: + - ../plugins/richie/apps/env.d/production + volumes: + - ../plugins/richie/apps/settings/tutor.py:/app/richie/sandbox/tutor.py:ro + - ../../data/richie/media:/data/media + depends_on: {{ [("elasticsearch", RUN_ELASTICSEARCH), ("lms", RUN_LMS), ("mysql", RUN_MYSQL)]|list_if }} diff --git a/tutorrichie/patches/nginx-extra b/tutorrichie/patches/nginx-extra new file mode 100644 index 0000000..f09cc38 --- /dev/null +++ b/tutorrichie/patches/nginx-extra @@ -0,0 +1,19 @@ +# Richie +upstream richie-backend { + server richie:8000 fail_timeout=0; +} +server { + listen 80; + server_name {{ RICHIE_HOST }}; + + # Disables server version feedback on pages and in headers + server_tokens off; + + client_max_body_size 10m; + + location / { + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://richie-backend; + } +} diff --git a/tutorrichie/patches/openedx-cms-development-settings b/tutorrichie/patches/openedx-cms-development-settings new file mode 100644 index 0000000..f26c804 --- /dev/null +++ b/tutorrichie/patches/openedx-cms-development-settings @@ -0,0 +1,6 @@ +# Richie settings +RICHIE_COURSE_HOOK = { + "secret": "{{ RICHIE_HOOK_SECRET }}", + "url": "http://richie:8003/api/v1.0/course-runs-sync/", + "timeout": 3, +} diff --git a/tutorrichie/patches/openedx-cms-production-settings b/tutorrichie/patches/openedx-cms-production-settings new file mode 100644 index 0000000..915b722 --- /dev/null +++ b/tutorrichie/patches/openedx-cms-production-settings @@ -0,0 +1,11 @@ +# Richie settings +RICHIE_ROOT_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ RICHIE_HOST }}" +MARKETING_SITE_ROOT = RICHIE_ROOT_URL +MKTG_URLS = { + "ROOT": RICHIE_ROOT_URL +} +RICHIE_COURSE_HOOK = { + "secret": "{{ RICHIE_HOOK_SECRET }}", + "url": "http://richie:8000/api/v1.0/course-runs-sync/", + "timeout": 3, +} diff --git a/tutorrichie/patches/openedx-common-settings b/tutorrichie/patches/openedx-common-settings new file mode 100644 index 0000000..5ea8ef0 --- /dev/null +++ b/tutorrichie/patches/openedx-common-settings @@ -0,0 +1,2 @@ +# Richie settings (common) +FEATURES["ENABLE_MKTG_SITE"] = True diff --git a/tutorrichie/patches/openedx-development-settings b/tutorrichie/patches/openedx-development-settings new file mode 100644 index 0000000..0314750 --- /dev/null +++ b/tutorrichie/patches/openedx-development-settings @@ -0,0 +1,6 @@ +# Richie (development) +RICHIE_ROOT_URL = "http://{{ RICHIE_HOST }}:8003" +MARKETING_SITE_ROOT = RICHIE_ROOT_URL +MKTG_URLS = { + "ROOT": RICHIE_ROOT_URL +} diff --git a/tutorrichie/patches/openedx-dockerfile-post-python-requirements b/tutorrichie/patches/openedx-dockerfile-post-python-requirements new file mode 100644 index 0000000..67ccbb5 --- /dev/null +++ b/tutorrichie/patches/openedx-dockerfile-post-python-requirements @@ -0,0 +1,2 @@ +# Install Richie catalog integration app +RUN pip install -e "git+https://github.com/overhangio/tutor-richie.git#egg=richie-edx-platform&subdirectory=contrib/edx-platform" diff --git a/tutorrichie/patches/openedx-lms-development-settings b/tutorrichie/patches/openedx-lms-development-settings new file mode 100644 index 0000000..9d50af4 --- /dev/null +++ b/tutorrichie/patches/openedx-lms-development-settings @@ -0,0 +1,2 @@ +# Richie (lms development) +CORS_ORIGIN_WHITELIST.append("{{ RICHIE_HOST }}:8003") diff --git a/tutorrichie/patches/openedx-lms-production-settings b/tutorrichie/patches/openedx-lms-production-settings new file mode 100644 index 0000000..52f3a4a --- /dev/null +++ b/tutorrichie/patches/openedx-lms-production-settings @@ -0,0 +1,7 @@ +# Richie settings (lms production) +RICHIE_ROOT_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ RICHIE_HOST }}" +MARKETING_SITE_ROOT = RICHIE_ROOT_URL +MKTG_URLS = { + "ROOT": RICHIE_ROOT_URL +} +CORS_ORIGIN_WHITELIST.append("{{ RICHIE_HOST }}") diff --git a/tutorrichie/plugin.py b/tutorrichie/plugin.py new file mode 100644 index 0000000..3e82f91 --- /dev/null +++ b/tutorrichie/plugin.py @@ -0,0 +1,41 @@ +from glob import glob +import os +import pkg_resources + +from .__about__ import __version__ + +templates = pkg_resources.resource_filename("tutorrichie", "templates") + +config = { + "add": { + "HOOK_SECRET": "{{ 20|random_string }}", + "SECRET_KEY": "{{ 20|random_string }}", + "MYSQL_PASSWORD": "{{ 12|random_string }}", + }, + "defaults": { + "VERSION": __version__, + "DOCKER_IMAGE": "{{ DOCKER_REGISTRY }}overhangio/openedx-richie:{{ RICHIE_VERSION }}", + "RELEASE_VERSION": "v2.8.2", + "HOST": "courses.{{ LMS_HOST }}", + "MYSQL_DATABASE": "richie", + "MYSQL_USERNAME": "richie", + "ELASTICSEARCH_INDICES_PREFIX": "richie", + }, +} + +hooks = { + "build-image": {"richie": "{{ RICHIE_DOCKER_IMAGE }}"}, + "remote-image": {"richie": "{{ RICHIE_DOCKER_IMAGE }}"}, + "init": ["mysql", "richie", "richie-openedx"], +} + + +def patches(): + all_patches = {} + patches_dir = pkg_resources.resource_filename("tutorrichie", "patches") + for path in glob(os.path.join(patches_dir, "*")): + with open(path) as patch_file: + name = os.path.basename(path) + content = patch_file.read() + all_patches[name] = content + return all_patches diff --git a/tutorrichie/templates/richie/apps/.gitignore b/tutorrichie/templates/richie/apps/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tutorrichie/templates/richie/apps/env.d/development b/tutorrichie/templates/richie/apps/env.d/development new file mode 100644 index 0000000..9c47208 --- /dev/null +++ b/tutorrichie/templates/richie/apps/env.d/development @@ -0,0 +1,3 @@ +DJANGO_CONFIGURATION=TutorDevelopment +EDX_BASE_URL=http://{{ LMS_HOST }}:8000 +AUTHENTICATION_BASE_URL=http://{{ LMS_HOST }}:8000 diff --git a/tutorrichie/templates/richie/apps/env.d/production b/tutorrichie/templates/richie/apps/env.d/production new file mode 100644 index 0000000..8c8a408 --- /dev/null +++ b/tutorrichie/templates/richie/apps/env.d/production @@ -0,0 +1,17 @@ +DJANGO_SETTINGS_MODULE=tutor +DJANGO_CONFIGURATION=TutorProduction +DJANGO_SECRET_KEY={{ RICHIE_SECRET_KEY}} +DJANGO_ALLOWED_HOSTS=richie,{{ RICHIE_HOST }} +DB_ENGINE=django.db.backends.mysql +DB_NAME={{ RICHIE_MYSQL_DATABASE }} +DB_USER={{ RICHIE_MYSQL_USERNAME }} +DB_PASSWORD={{ RICHIE_MYSQL_PASSWORD }} +DB_HOST={{ MYSQL_HOST }} +DB_PORT={{ MYSQL_PORT }} +# Note: richie does not support ES authentication, yet +RICHIE_ES_HOST={{ ELASTICSEARCH_HOST }} +RICHIE_ES_INDICES_PREFIX={{ RICHIE_ELASTICSEARCH_INDICES_PREFIX }} +EDX_BASE_URL={% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }} +EDX_JS_BACKEND=openedx-hawthorn +AUTHENTICATION_BASE_URL={% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }} +AUTHENTICATION_BACKEND=openedx-hawthorn diff --git a/tutorrichie/templates/richie/apps/settings/tutor.py b/tutorrichie/templates/richie/apps/settings/tutor.py new file mode 100644 index 0000000..5a489ca --- /dev/null +++ b/tutorrichie/templates/richie/apps/settings/tutor.py @@ -0,0 +1,71 @@ +from django.conf import global_settings, settings +from django.utils.translation import gettext_lazy as _ + +from configurations import values +from settings import Development, Production + +supported_languages = [ + ("en", _("English")), + {% if LANGUAGE_CODE != "en" %}("{{ LANGUAGE_CODE }}", _(dict(global_settings.LANGUAGES)["{{ LANGUAGE_CODE }}"])),{% endif %} +] + +class TutorSettingsMixin: + RICHIE_COURSE_RUN_SYNC_SECRETS = values.ListValue(["{{ RICHIE_HOOK_SECRET }}"]) + # Restore error logging, which is disabled by default + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "root": { + "handlers": ["console"], + "level": "WARNING", + }, + } + LANGUAGE_CODE = "{{ LANGUAGE_CODE }}" + LANGUAGES = supported_languages + CMS_LANGUAGES = { + "default": { + "public": True, + "hide_untranslated": False, + "redirect_on_fallback": False, + "fallbacks": [language[0] for language in supported_languages], + }, + 1: [ + { + "public": True, + "code": language[0], + "hide_untranslated": False, + "name": language[1], + "fallbacks": [supported_languages[0][0]], + "redirect_on_fallback": False, + } + for language in supported_languages + ] + } + PARLER_LANGUAGES = CMS_LANGUAGES + + {{ patch("richie-settings-common")|indent(4) }} + +class TutorProduction(TutorSettingsMixin, Production): + """ + Tutor-specific settings for production. + """ + {% if not ENABLE_HTTPS %} + CSRF_COOKIE_SECURE = False + SECURE_BROWSER_XSS_FILTER = False + SECURE_CONTENT_TYPE_NOSNIFF = False + SESSION_COOKIE_SECURE = False + {% endif %} + + {{ patch("richie-settings-production")|indent(4) }} + + +class TutorDevelopment(TutorSettingsMixin, Development): + """ + Tutor-specific settings for development. + """ + {{ patch("richie-settings-development")|indent(4) }} diff --git a/tutorrichie/templates/richie/build/.gitignore b/tutorrichie/templates/richie/build/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tutorrichie/templates/richie/build/richie/Dockerfile b/tutorrichie/templates/richie/build/richie/Dockerfile new file mode 100644 index 0000000..eea4140 --- /dev/null +++ b/tutorrichie/templates/richie/build/richie/Dockerfile @@ -0,0 +1,80 @@ +#--------- Base image with cloned repo +FROM python:3.7-bullseye as base + +RUN apt update \ + && apt install -y git \ + && rm -rf /var/lib/apt/lists/* + +# Clone repo +ARG RICHIE_REPOSITORY=https://github.com/openfun/richie.git +ARG RICHIE_VERSION={{ RICHIE_RELEASE_VERSION }} +RUN git clone $RICHIE_REPOSITORY --branch $RICHIE_VERSION --depth 1 /richie + +#--------- Front-end builder image +FROM node:14 as frontend-builder + +COPY --from=base /richie/src/frontend /app/richie/src/frontend +WORKDIR /app/richie/src/frontend +RUN yarn install --frozen-lockfile && \ + yarn compile-translations && \ + yarn build-ts-production && \ + yarn build-sass-production + +#--------- Production image +FROM python:3.7-bullseye as production + +RUN apt update \ + && apt install -y gettext git default-mysql-client \ + && rm -rf /var/lib/apt/lists/* + +# User creation +RUN useradd --home-dir /app --create-home --uid=1000 openedx +RUN mkdir -p /data/media /data/static && chown -R openedx:openedx /data +USER openedx + +COPY --from=base --chown=openedx:openedx /richie /app/richie +WORKDIR /app/richie + +# Install project (with requirements) +RUN python -m venv /app/venv +ENV PATH /app/venv/bin:${PATH} +RUN pip install pip==21.2.4 setuptools==58.0.4 wheel==0.37.0 +RUN pip install -e .[sandbox] +RUN pip install uwsgi==2.0.19.1 +RUN pip install mysqlclient==2.0.3 +# Use temporarily a forked version of djangocms-admin-style +# Remove this when djangocms-admin-style 2.0.3 will be released\ +# See upstream Dockerfile https://github.com/openfun/richie/blob/master/Dockerfile +RUN pip install --prefix=/install git+https://github.com/jbpenrath/djangocms-admin-style@fun#egg=djangocms-admin-style +# Install requirements for storing media assets on S3/MinIO +RUN pip install django-storages==1.12.1 boto3==1.18.60 + +ENV DJANGO_SETTINGS_MODULE settings +ENV DJANGO_CONFIGURATION Production +ENV DJANGO_SECRET_KEY setme + + +# Collect static assets +COPY --from=frontend-builder --chown=openedx:openedx \ + /app/richie/src/richie/static/richie/js \ + /app/richie/src/richie/static/richie/js +COPY --from=frontend-builder --chown=openedx:openedx \ + /app/richie/src/richie/static/richie/css/main.css \ + /app/richie/src/richie/static/richie/css/main.css +RUN ./sandbox/manage.py collectstatic + +# Compile translations +RUN ./sandbox/manage.py compilemessages + +# Run server +EXPOSE 8000 +CMD cd sandbox && uwsgi \ + --static-map /static=/data/static/ \ + --static-map /media=/data/media/ \ + --http 0.0.0.0:8000 \ + --thunder-lock \ + --single-interpreter \ + --enable-threads \ + --processes=${UWSGI_WORKERS:-2} \ + --buffer-size=8192 \ + --wsgi-file wsgi.py diff --git a/tutorrichie/templates/richie/hooks/.gitignore b/tutorrichie/templates/richie/hooks/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tutorrichie/templates/richie/hooks/mysql/init b/tutorrichie/templates/richie/hooks/mysql/init new file mode 100644 index 0000000..fca51c8 --- /dev/null +++ b/tutorrichie/templates/richie/hooks/mysql/init @@ -0,0 +1,2 @@ +mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e 'CREATE DATABASE IF NOT EXISTS {{ RICHIE_MYSQL_DATABASE }};' +mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e 'GRANT ALL ON {{ RICHIE_MYSQL_DATABASE }}.* TO "{{ RICHIE_MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ RICHIE_MYSQL_PASSWORD }}";' diff --git a/tutorrichie/templates/richie/hooks/richie-openedx/init b/tutorrichie/templates/richie/hooks/richie-openedx/init new file mode 100644 index 0000000..5d88369 --- /dev/null +++ b/tutorrichie/templates/richie/hooks/richie-openedx/init @@ -0,0 +1,2 @@ +# Initial sync of course runs +./manage.py cms shell -c "from richie.sync import sync_all_courses; sync_all_courses()" diff --git a/tutorrichie/templates/richie/hooks/richie/init b/tutorrichie/templates/richie/hooks/richie/init new file mode 100644 index 0000000..a9ed718 --- /dev/null +++ b/tutorrichie/templates/richie/hooks/richie/init @@ -0,0 +1,11 @@ +# Fix media permissions +chown -R openedx:openedx /data/media + +# Create tables +./sandbox/manage.py migrate + +# Create ES indices +./sandbox/manage.py bootstrap_elasticsearch + +# Create required pages +./sandbox/manage.py richie_init