Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cohort certificate overrides filter #50

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ clean: ## delete most git-ignored files
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
rm -rf venv +
echo "cleaned"
# rm -rf venv +

virtual_environment: ## create virtual environment
test -d venv || virtualenv venv --python=python3
Expand Down
72 changes: 72 additions & 0 deletions nau_openedx_extensions/certificates/context_overrides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
This file defines overrides of the context render of course certificates using an Open edX Filters pipeline step.
"""

import logging

from openedx_filters import PipelineStep

from nau_openedx_extensions.edxapp_wrapper.cohort import get_cohort

log = logging.getLogger(__name__)


class CertificatesContextCohortOverride(PipelineStep):
"""
Override the certificates render template context with information from the student cohort.
If user has a cohort and that cohort has custom certificate overrides, then override the root context variables
with the cohorted ones.

Example usage:
Add the following configurations to your configuration file:
"OPEN_EDX_FILTERS_CONFIG": {
"org.openedx.learning.certificate.render.started.v1": {
"fail_silently": false,
"pipeline": [
"nau_openedx_extensions.certificates.context_overrides.CertificatesContextCohortOverride"
]
}
}

Configure course on field "Certificate Web/HTML View Overrides" with:
{
"footer_additional_logo": "https://lms.example.com/some_logo.png",
"cohort_overrides": {
"test": {
"footer_additional_logo": "https://lms.example.com/override_logo.png"
}
}
}
"""

def run_filter(self, context, custom_template): # pylint: disable=arguments-differ
"""
The filter logic.
"""
username = context["username"]
course_key = context["course_id"]
if "cohort_overrides" in context:
cohort = get_cohort(username, course_key)
if cohort:
if cohort.name in context["cohort_overrides"]:
cohort_override_dict = context["cohort_overrides"][cohort.name]
context.update(cohort_override_dict)
else:
log.info(
"The user '%s' enrollment on course '%s' doesn't have a cohort "
"certificate context overrides configured for the cohort '%s'.",
username,
course_key,
cohort.name,
)
else:
log.info(
"User '%s' not in a cohort on course '%s'", username, course_key
)
else:
log.info(
"No Certificates context cohort_overrides defined on course '%s'",
course_key,
)

return {"context": context, "custom_template": custom_template}
14 changes: 14 additions & 0 deletions nau_openedx_extensions/edxapp_wrapper/backends/cohort_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
Cohort abstraction backend
"""
from common.djangoapps.student.models import get_user_by_username_or_email # pylint: disable=import-error
from openedx.core.djangoapps.course_groups.cohorts import \
get_cohort as edxapp_get_cohort # pylint: disable=import-error


def get_cohort(username, course_key):
"""
Get the Course Cohort for the User that belongs the username if available other case return None.
"""
user = get_user_by_username_or_email(username)
return edxapp_get_cohort(user, course_key, assign=False, use_cached=False)
10 changes: 10 additions & 0 deletions nau_openedx_extensions/edxapp_wrapper/backends/cohort_v1_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Cohort abstraction backend
"""


def get_cohort(username, course_key): # pylint: disable=unused-argument
"""
For tests.
"""
return None
15 changes: 15 additions & 0 deletions nau_openedx_extensions/edxapp_wrapper/cohort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
""" CourseMetadata backend abstraction """

from importlib import import_module

from django.conf import settings


def get_cohort(*args, **kwargs):
"""
Get the Course Cohort for the User that belongs the username if available other case return None.
"""
backend_module = settings.NAU_COHORT_MODULE
backend = import_module(backend_module)

return backend.get_cohort(*args, **kwargs)
6 changes: 3 additions & 3 deletions nau_openedx_extensions/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: [email protected]\n"
"POT-Creation-Date: 2024-05-10 14:45+0100\n"
"POT-Creation-Date: 2024-09-26 11:25+0100\n"
"PO-Revision-Date: 2021-02-15 15:56+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
Expand Down Expand Up @@ -133,11 +133,11 @@ msgid ""
"out in order to obtain a certificate."
msgstr ""

#: nau_openedx_extensions/settings/common.py:86
#: nau_openedx_extensions/settings/common.py:89
msgid "Certificate"
msgstr ""

#: nau_openedx_extensions/settings/common.py:87
#: nau_openedx_extensions/settings/common.py:90
msgid "Certificate of Achievement"
msgstr ""

Expand Down
6 changes: 3 additions & 3 deletions nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: [email protected]\n"
"POT-Creation-Date: 2024-05-10 14:45+0100\n"
"POT-Creation-Date: 2024-09-26 11:25+0100\n"
"PO-Revision-Date: 2021-02-15 15:56+0000\n"
"Last-Translator: Ivo Branco <[email protected]>\n"
"Language: pt_PT\n"
Expand Down Expand Up @@ -142,11 +142,11 @@ msgstr ""
"Este curso encontra-se arquivado e já não permite a realização de "
"atividades para obtenção de certificado."

#: nau_openedx_extensions/settings/common.py:86
#: nau_openedx_extensions/settings/common.py:89
msgid "Certificate"
msgstr "Certificado"

#: nau_openedx_extensions/settings/common.py:87
#: nau_openedx_extensions/settings/common.py:90
msgid "Certificate of Achievement"
msgstr "Certificado de Conclusão"

Expand Down
3 changes: 3 additions & 0 deletions nau_openedx_extensions/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def plugin_settings(settings):
settings.NAU_STUDENT_MODULE = (
"nau_openedx_extensions.edxapp_wrapper.backends.student_l_v1"
)
settings.NAU_COHORT_MODULE = (
"nau_openedx_extensions.edxapp_wrapper.backends.cohort_v1"
)

# Overwrite the default certificate name
settings.CERT_NAME_SHORT = _("Certificate")
Expand Down
4 changes: 4 additions & 0 deletions nau_openedx_extensions/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ class SettingsClass:
NAU_STUDENT_MODULE = (
"nau_openedx_extensions.edxapp_wrapper.backends.student_l_v1_tests"
)

NAU_COHORT_MODULE = (
"nau_openedx_extensions.edxapp_wrapper.backends.cohort_v1_tests"
)
153 changes: 153 additions & 0 deletions nau_openedx_extensions/tests/test_certificates_context_overrides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
Tests for certificates context overrides.
"""
from unittest.mock import MagicMock, PropertyMock, patch

from django.test import TestCase

from nau_openedx_extensions.certificates.context_overrides import CertificatesContextCohortOverride


class CertificatesContextOverridesTest(TestCase):
"""
Test certificates context overrides for cohorts.
The override configuration is managed on the <CMS_HOST>/settings/advanced/<course_id>
on field name `cert_html_view_overrides`.
"""

@patch('nau_openedx_extensions.certificates.context_overrides.get_cohort')
def test_certificates_context_overrides_no_cohort_and_no_override(self, get_cohort_mock):
"""
Check if no cohort and no override.
"""
get_cohort_mock.return_value = None

context = {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
}
result = CertificatesContextCohortOverride.run_filter(None, context, "some_template")
get_cohort_mock.assert_not_called()

self.assertDictEqual(result['context'], {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
})

@patch('nau_openedx_extensions.certificates.context_overrides.get_cohort')
def test_certificates_context_overrides_with_cohort_no_override(self, get_cohort_mock):
"""
Check that no override is applied when the learner has a cohort but no override is configured.
"""
mocked_cohort = MagicMock()
cohort_name_property = PropertyMock(return_value="SomeGroup")
type(mocked_cohort).name = cohort_name_property
get_cohort_mock.return_value = mocked_cohort

context = {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
}
CertificatesContextCohortOverride.run_filter(None, context, "some_template")
get_cohort_mock.assert_not_called()

@patch('nau_openedx_extensions.certificates.context_overrides.get_cohort')
def test_certificates_context_overrides_with_no_cohort_and_with_override(self, get_cohort_mock):
"""
Check that no override is applied when the learner hasn't a cohort and the course has an override.
"""
get_cohort_mock.return_value = None

context = {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
"footer_additional_logo": "http://lms.example.com/base_logo.png",
"cohort_overrides": {
"SomeGroup": {
"footer_additional_logo": "http://lms.example.com/override_logo.png",
},
},
}
result = CertificatesContextCohortOverride.run_filter(None, context, "some_template")
get_cohort_mock.assert_called_once_with("[email protected]", "course-v1:Demo+DemoX+Demo_Course")

self.assertDictEqual(result['context'], {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
"footer_additional_logo": "http://lms.example.com/base_logo.png",
"cohort_overrides": {
"SomeGroup": {
"footer_additional_logo": "http://lms.example.com/override_logo.png",
},
},
})

@patch('nau_openedx_extensions.certificates.context_overrides.get_cohort')
def test_certificates_context_overrides_with_cohort_and_override_dont_match(self, get_cohort_mock):
"""
Check that the override isn't being applied when the learner belongs to a cohort that isn't configured on the
override.
"""
mocked_cohort = MagicMock()
cohort_name_property = PropertyMock(return_value="some_group_not_configured_on_override")
type(mocked_cohort).name = cohort_name_property
get_cohort_mock.return_value = mocked_cohort

context = {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
"footer_additional_logo": "http://lms.example.com/base_logo.png",
"cohort_overrides": {
"SomeGroup": {
"footer_additional_logo": "http://lms.example.com/override_logo.png",
},
},
}
result = CertificatesContextCohortOverride.run_filter(None, context, "some_template")
get_cohort_mock.assert_called_once_with("[email protected]", "course-v1:Demo+DemoX+Demo_Course")

self.assertDictEqual(result['context'], {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
"footer_additional_logo": "http://lms.example.com/base_logo.png",
"cohort_overrides": {
"SomeGroup": {
"footer_additional_logo": "http://lms.example.com/override_logo.png",
},
},
})

@patch('nau_openedx_extensions.certificates.context_overrides.get_cohort')
def test_certificates_context_overrides_with_cohort_and_override(self, get_cohort_mock):
"""
Check that the override is being applied if the learner belongs to a cohort and that cohort
is configured has override.
"""
mocked_cohort = MagicMock()
cohort_name_property = PropertyMock(return_value="SomeGroup")
type(mocked_cohort).name = cohort_name_property
get_cohort_mock.return_value = mocked_cohort

context = {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
"footer_additional_logo": "http://lms.example.com/base_logo.png",
"cohort_overrides": {
"SomeGroup": {
"footer_additional_logo": "http://lms.example.com/override_logo.png",
},
},
}
result = CertificatesContextCohortOverride.run_filter(None, context, "some_template")
get_cohort_mock.assert_called_once_with("[email protected]", "course-v1:Demo+DemoX+Demo_Course")

self.assertDictEqual(result['context'], {
"username": "[email protected]",
"course_id": "course-v1:Demo+DemoX+Demo_Course",
"footer_additional_logo": "http://lms.example.com/override_logo.png",
"cohort_overrides": {
"SomeGroup": {
"footer_additional_logo": "http://lms.example.com/override_logo.png",
},
},
})
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[tool:pytest]
DJANGO_SETTINGS_MODULE = nau_openedx_extensions.settings.test

[pytest]
log_cli = 1
log_cli_level = INFO
Loading