Skip to content
Open
103 changes: 103 additions & 0 deletions src/ol_openedx_git_auto_export/CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Configuration Guide: Feature Flags

This document describes the available feature flags for controlling git auto-export behavior for courses and libraries.

## Feature Flags Overview

### Course-Specific Flags

#### `ENABLE_GIT_AUTO_EXPORT`
- **Type**: Boolean
- **Default**: `True`
- **Purpose**: Controls automatic git export for courses when they are published
- **Scope**: Courses only (unless library flag not set, see below)
- **Location**: `settings.FEATURES['ENABLE_GIT_AUTO_EXPORT']`

**Example**:
```python
FEATURES['ENABLE_GIT_AUTO_EXPORT'] = True
```

#### `ENABLE_AUTO_GITHUB_REPO_CREATION`
- **Type**: Boolean
- **Default**: `False`
- **Purpose**: Controls automatic GitHub repository creation for new courses
- **Scope**: Courses only (unless library flag not set, see below)
- **Location**: `settings.FEATURES['ENABLE_AUTO_GITHUB_REPO_CREATION']`

**Example**:
```python
FEATURES['ENABLE_AUTO_GITHUB_REPO_CREATION'] = True
```

### Library-Specific Flags

#### `ENABLE_GIT_AUTO_LIBRARY_EXPORT`
- **Type**: Boolean
- **Default**: False
- **Purpose**: Controls automatic git export for libraries when they are updated
- **Scope**: Libraries only
- **Location**: `settings.FEATURES['ENABLE_GIT_AUTO_LIBRARY_EXPORT']`

**Example**:
```python
# Enable library export separately from courses
FEATURES['ENABLE_GIT_AUTO_LIBRARY_EXPORT'] = True

# Or disable library export while keeping course export enabled
FEATURES['ENABLE_GIT_AUTO_EXPORT'] = True
FEATURES['ENABLE_GIT_AUTO_LIBRARY_EXPORT'] = False
```

#### `ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION`
- **Type**: Boolean
- **Default**: False
- **Purpose**: Controls automatic GitHub repository creation for new libraries
- **Scope**: Libraries only
- **Location**: `settings.FEATURES['ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION']`

**Example**:
```python
# Enable library repo creation separately from courses
FEATURES['ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION'] = True

# Or disable library repo creation while keeping course repo creation enabled
FEATURES['ENABLE_AUTO_GITHUB_REPO_CREATION'] = True
FEATURES['ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION'] = False
```

### Required Settings (for both courses and libraries)

#### `ENABLE_EXPORT_GIT`
- **Type**: Boolean
- **Purpose**: Master switch for git export functionality
- **Note**: Must be enabled for any git export to work
- **Location**: `settings.FEATURES['ENABLE_EXPORT_GIT']`

#### `GITHUB_ORG_API_URL`
- **Type**: String (URL)
- **Purpose**: GitHub organization API URL for creating repositories
- **Example**: `https://api.github.com/orgs/your-org`
- **Location**: `settings.GITHUB_ORG_API_URL`

#### `GITHUB_ACCESS_TOKEN`
- **Type**: String (Token)
- **Purpose**: GitHub personal access token with repo creation permissions
- **Location**: `settings.GITHUB_ACCESS_TOKEN`
- **Security**: Should be stored securely (e.g., environment variable, secrets management)

#### `GIT_REPO_EXPORT_DIR`
- **Type**: String (Path)
- **Purpose**: Directory path for git export operations
- **Default**: `/openedx/export_course_repos`
- **Location**: `settings.GIT_REPO_EXPORT_DIR`

## Security Considerations

- **Never commit `GITHUB_ACCESS_TOKEN` to version control**
- Use environment variables or secrets management
- Ensure GitHub token has minimum required permissions:
- `repo` scope for repository creation
- `write:org` if creating repos in an organization
- Rotate tokens regularly
- Use different tokens for different environments (dev/staging/prod)
59 changes: 58 additions & 1 deletion src/ol_openedx_git_auto_export/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,68 @@ https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account)

Studio/CMS UI settings
----------------------
- Open studio admin at `/admin/ol_openedx_git_auto_export/coursegitrepository/`
- Open studio admin at ``/admin/ol_openedx_git_auto_export/contentgitrepository/``
- Add your course_id and in the GIT URL, add your OLX git repo. For example ``git@github.com:<GITHUB_USERNAME>/edx4edxlite.git``.
- Make a change to the course content and publish.
- When using Tutor, attach with the CMS container using ``tutor dev/local start cms`` and enter ``yes`` to the prompt to add the GitHub to known hosts.
- You should see a new commit in your OLX repo.
- Commit user should be the one that published the change.
- If user is not available, it should be the default one set in ``GIT_EXPORT_DEFAULT_IDENT``.
- Test commit count increase on your OLX repo.
Library Support
===============

This plugin supports automatic git export for both **courses** and **content libraries**.

Library v1 (Legacy Libraries)
------------------------------

**Library Key Format:** ``library-v1:org+library``

**Supported Operations:**

- ✅ **Auto-export on update**: When you update/publish a library v1, changes are automatically exported to git
- ✅ **Auto-repo creation**: Library v1 does NOT have a creation signal in Open edX, so the GitHub repository is automatically created on the first library update signal

**Setup for Library v1:**

1. Enable the library feature flags in your config:

.. code-block::

"FEATURES": {
"ENABLE_GIT_AUTO_LIBRARY_EXPORT": true,
"ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION": true
}

2. Make changes to your library and publish

Library v2 (Content Libraries)
-------------------------------

**Library Key Format:** ``lib:org:slug``

**Supported Operations:**

- ✅ **Auto-export on update**: When you update a library v2, changes are automatically exported to git
- ✅ **Auto-repo creation**: When you create a new library v2, a GitHub repository can be automatically created

**Setup for Library v2:**

1. Enable the library feature flags in your config:

.. code-block::

"FEATURES": {
"ENABLE_GIT_AUTO_LIBRARY_EXPORT": true,
"ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION": true
}

2. When creating a new library v2, if ``ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION`` is enabled, a GitHub repository will be created automatically
3. Updates to the library will be automatically exported to git

**Important Notes:**

- Library v1 does not emit a creation signal, so the GitHub repository is automatically created on the first library update
- Library v2 uses the new event architecture and supports both creation and update signals for immediate repository creation
- Both library types support automatic git export on updates once configured
69 changes: 58 additions & 11 deletions src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,79 @@
"""

from django.contrib import admin
from django.contrib.admin import SimpleListFilter
from django.db.models import Q
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2

from ol_openedx_git_auto_export.models import CourseGitRepository
from ol_openedx_git_auto_export.constants import (
LIBRARY_V1_PREFIX,
LIBRARY_V2_PREFIX,
ContentType,
)
from ol_openedx_git_auto_export.models import ContentGitRepository


@admin.register(CourseGitRepository)
class CourseGitRepositoryAdmin(admin.ModelAdmin):
class ContentTypeFilter(SimpleListFilter):
"""Filter for content type (Course or Library)."""

title = "content type"
parameter_name = "content_type"

def lookups(self, request, model_admin): # noqa: ARG002
"""Return filter options."""
return (
(ContentType.COURSE.value, ContentType.COURSE.display_name),
(ContentType.LIBRARY.value, ContentType.LIBRARY.display_name),
)

def queryset(self, request, queryset): # noqa: ARG002
"""Filter the queryset based on the selected content type."""
if self.value() == ContentType.COURSE.value:
# Filter for courses (exclude libraries)
return queryset.exclude(content_key__startswith=LIBRARY_V1_PREFIX).exclude(
content_key__startswith=LIBRARY_V2_PREFIX
)
elif self.value() == ContentType.LIBRARY.value:
# Filter for libraries
return queryset.filter(
Q(content_key__startswith=LIBRARY_V1_PREFIX)
| Q(content_key__startswith=LIBRARY_V2_PREFIX)
)
return queryset


@admin.register(ContentGitRepository)
class ContentGitRepositoryAdmin(admin.ModelAdmin):
"""
Admin interface for the CourseGitRepository model.
Admin interface for the ContentGitRepository model.

This model supports both courses and libraries.
"""

list_display = (
"course_key",
"content_key",
"git_url",
"is_export_enabled",
"content_type_display",
)
search_fields = ("course_key", "git_url")
list_filter = ("is_export_enabled",)
search_fields = ("content_key", "git_url")
list_filter = ("is_export_enabled", ContentTypeFilter)
list_per_page = 50

@admin.display(description="Content Type")
def content_type_display(self, obj):
"""Display whether the content is a course or library."""
if isinstance(obj.content_key, (LibraryLocator, LibraryLocatorV2)):
return ContentType.LIBRARY.display_name
return ContentType.COURSE.display_name

def has_delete_permission(self, request, obj=None): # noqa: ARG002
"""
Disable delete permission for CourseGitRepository objects.
Disable delete permission for ContentGitRepository objects.

Deleting a CourseGitRepository could lead to orphaned repositories
on GitHub and loss of course export functionality.
Deleting a ContentGitRepository could lead to orphaned repositories
on GitHub and loss of export functionality.

To stop exporting a course, set `is_export_enabled` to False
To stop exporting content, set `is_export_enabled` to False
"""
return False
43 changes: 39 additions & 4 deletions src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
AppConfig for ol_openedx_git_auto_export app
"""
# ruff: noqa: E501

from django.apps import AppConfig
from edx_django_utils.plugins import PluginSettings, PluginSignals
Expand Down Expand Up @@ -28,13 +29,47 @@ class GitAutoExportConfig(AppConfig):
PluginSignals.RECEIVERS: [
{
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_course_publish",
PluginSignals.SIGNAL_PATH: "xmodule.modulestore.django.COURSE_PUBLISHED", # noqa: E501
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_course_publish", # noqa: E501
PluginSignals.SIGNAL_PATH: "xmodule.modulestore.django.COURSE_PUBLISHED",
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_course_publish",
},
{
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_course_created",
PluginSignals.SIGNAL_PATH: "openedx_events.content_authoring.signals.COURSE_CREATED", # noqa: E501
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_course_created", # noqa: E501
PluginSignals.SIGNAL_PATH: "openedx_events.content_authoring.signals.COURSE_CREATED",
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_course_created",
},
# Library Signals
# NOTE: Library v1 (library-v1:) only has LIBRARY_UPDATED, no creation signal
# Library v2 (lib:) has:
# - CONTENT_LIBRARY_CREATED/UPDATED: for library metadata changes
# - LIBRARY_BLOCK_PUBLISHED: for block/component changes
{
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_library_v1_updated",
PluginSignals.SIGNAL_PATH: "xmodule.modulestore.django.LIBRARY_UPDATED", # library v1 update
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_library_v1_updated",
},
# lib V2 - Library-level signals
{
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_library_v2_created",
PluginSignals.SIGNAL_PATH: "openedx_events.content_authoring.signals.CONTENT_LIBRARY_CREATED", # library v2 only
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_library_v2_created",
},
{
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_library_v2_updated",
PluginSignals.SIGNAL_PATH: "openedx_events.content_authoring.signals.CONTENT_LIBRARY_UPDATED", # library v2 metadata only
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_library_v2_updated",
},
# lib V2 - Block-level signals (for component publish)
# Note: PUBLISHED signals capture all changes including deletions after publish
{
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_library_block_published",
PluginSignals.SIGNAL_PATH: "openedx_events.content_authoring.signals.LIBRARY_BLOCK_PUBLISHED",
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_library_block_published",
},
# lib V2 - Container-level signals (for container publish)
{
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_library_container_published",
PluginSignals.SIGNAL_PATH: "openedx_events.content_authoring.signals.LIBRARY_CONTAINER_PUBLISHED",
PluginSignals.DISPATCH_UID: "ol_openedx_git_auto_export.signals.listen_for_library_container_published",
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
from enum import StrEnum


class ContentType(StrEnum):
"""Enumeration for content types (Course or Library)."""

COURSE = "course"
LIBRARY = "library"

@property
def display_name(self):
"""Return the human-readable display name."""
return self.value.capitalize()


# Library key prefixes for different versions
LIBRARY_V1_PREFIX = "library-v1:"
LIBRARY_V2_PREFIX = "lib:"

ENABLE_GIT_AUTO_EXPORT = "ENABLE_GIT_AUTO_EXPORT"
ENABLE_AUTO_GITHUB_REPO_CREATION = "ENABLE_AUTO_GITHUB_REPO_CREATION"
GITHUB_ORG = "GITHUB_ORG"
GITHUB_ACCESS_TOKEN = "GITHUB_ACCESS_TOKEN" # noqa: S105

# Library-specific feature flags
ENABLE_GIT_AUTO_LIBRARY_EXPORT = "ENABLE_GIT_AUTO_LIBRARY_EXPORT"
ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION = "ENABLE_AUTO_GITHUB_LIBRARY_REPO_CREATION"

COURSE_RERUN_STATE_SUCCEEDED = "succeeded"
REPOSITORY_NAME_MAX_LENGTH = 100 # Max length from GitHub for repo name
Loading
Loading