From b65a6457a6e72d7969127355144612f2e2b626d7 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Sun, 27 Aug 2023 20:38:16 +0200 Subject: [PATCH 01/46] style: add ruff --- .readthedocs.yml | 10 +- Makefile | 6 +- codecov.yml | 8 +- openedx_certificates/__init__.py | 4 +- openedx_certificates/apps.py | 15 +- openedx_certificates/urls.py | 10 +- pylintrc | 390 ------------------------------- pylintrc_tweaks | 11 - pyproject.toml | 134 ++++++++++- requirements/dev.txt | 80 +------ requirements/doc.txt | 3 + requirements/quality.in | 4 - requirements/quality.txt | 62 +---- requirements/test.in | 1 + requirements/test.txt | 6 +- setup.cfg | 10 - setup.py | 76 +++--- test_settings.py | 18 +- tests/test_models.py | 21 +- tox.ini | 18 +- 20 files changed, 238 insertions(+), 649 deletions(-) delete mode 100644 pylintrc delete mode 100644 pylintrc_tweaks delete mode 100644 setup.cfg diff --git a/.readthedocs.yml b/.readthedocs.yml index 0f029d2..e273e92 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,7 +9,15 @@ version: 2 sphinx: configuration: docs/conf.py +build: + os: ubuntu-22.04 + tools: + python: "3.8" + python: - version: 3.8 install: - requirements: requirements/doc.txt + +formats: + - epub + - pdf diff --git a/Makefile b/Makefile index 3bf6b7e..f5bffe9 100644 --- a/Makefile +++ b/Makefile @@ -72,10 +72,8 @@ test: clean ## run tests in the current virtualenv diff_cover: test ## find diff lines that need test coverage diff-cover coverage.xml -test-all: quality pii_check ## run tests on every supported Python/Django combination - tox - tox -e docs - tox -e package +test-all: ## run all tests + tox --parallel validate: quality pii_check test ## run tests and quality checks diff --git a/codecov.yml b/codecov.yml index 4da4768..16e67ae 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,11 +2,11 @@ coverage: status: project: default: - enabled: yes - target: auto + enabled: true + target: 80% patch: default: - enabled: yes - target: 100% + enabled: true + target: 80% comment: false diff --git a/openedx_certificates/__init__.py b/openedx_certificates/__init__.py index dcbbdd2..de41cfc 100644 --- a/openedx_certificates/__init__.py +++ b/openedx_certificates/__init__.py @@ -1,5 +1,3 @@ -""" -A pluggable service for preparing Open edX certificates. -""" +"""A pluggable service for preparing Open edX certificates.""" __version__ = '0.1.0' diff --git a/openedx_certificates/apps.py b/openedx_certificates/apps.py index da04290..5997f54 100644 --- a/openedx_certificates/apps.py +++ b/openedx_certificates/apps.py @@ -1,13 +1,16 @@ -""" -openedx_certificates Django application initialization. -""" +"""openedx_certificates Django application initialization.""" + +from __future__ import annotations + +from typing import ClassVar from django.apps import AppConfig class OpenedxCertificatesConfig(AppConfig): - """ - Configuration for the openedx_certificates Django application. - """ + """Configuration for the openedx_certificates Django application.""" name = 'openedx_certificates' + + # https://edx.readthedocs.io/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html + plugin_app: ClassVar[dict[str, dict[str, dict]]] = {} diff --git a/openedx_certificates/urls.py b/openedx_certificates/urls.py index b6c8226..77c6601 100644 --- a/openedx_certificates/urls.py +++ b/openedx_certificates/urls.py @@ -1,10 +1,8 @@ -""" -URLs for openedx_certificates. -""" -# from django.urls import re_path -# from django.views.generic import TemplateView +"""URLs for openedx_certificates.""" +# from django.urls import re_path # noqa: ERA001, RUF100 +# from django.views.generic import TemplateView # noqa: ERA001, RUF100 urlpatterns = [ # TODO: Fill in URL patterns and views here. - # re_path(r'', TemplateView.as_view(template_name="openedx_certificates/base.html")), + # re_path(r'', TemplateView.as_view(template_name="openedx_certificates/base.html")), # noqa: ERA001, RUF100 ] diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 4e9dcce..0000000 --- a/pylintrc +++ /dev/null @@ -1,390 +0,0 @@ -# *************************** -# ** DO NOT EDIT THIS FILE ** -# *************************** -# -# This file was generated by edx-lint: https://github.com/openedx/edx-lint -# -# If you want to change this file, you have two choices, depending on whether -# you want to make a local change that applies only to this repo, or whether -# you want to make a central change that applies to all repos using edx-lint. -# -# Note: If your pylintrc file is simply out-of-date relative to the latest -# pylintrc in edx-lint, ensure you have the latest edx-lint installed -# and then follow the steps for a "LOCAL CHANGE". -# -# LOCAL CHANGE: -# -# 1. Edit the local pylintrc_tweaks file to add changes just to this -# repo's file. -# -# 2. Run: -# -# $ edx_lint write pylintrc -# -# 3. This will modify the local file. Submit a pull request to get it -# checked in so that others will benefit. -# -# -# CENTRAL CHANGE: -# -# 1. Edit the pylintrc file in the edx-lint repo at -# https://github.com/openedx/edx-lint/blob/master/edx_lint/files/pylintrc -# -# 2. install the updated version of edx-lint (in edx-lint): -# -# $ pip install . -# -# 3. Run (in edx-lint): -# -# $ edx_lint write pylintrc -# -# 4. Make a new version of edx_lint, submit and review a pull request with the -# pylintrc update, and after merging, update the edx-lint version and -# publish the new version. -# -# 5. In your local repo, install the newer version of edx-lint. -# -# 6. Run: -# -# $ edx_lint write pylintrc -# -# 7. This will modify the local file. Submit a pull request to get it -# checked in so that others will benefit. -# -# -# -# -# -# STAY AWAY FROM THIS FILE! -# -# -# -# -# -# SERIOUSLY. -# -# ------------------------------ -# Generated by edx-lint version: 5.3.4 -# ------------------------------ -[MASTER] -ignore = migrations -persistent = yes -load-plugins = edx_lint.pylint,pylint_django,pylint_celery - -[MESSAGES CONTROL] -enable = - blacklisted-name, - line-too-long, - - abstract-class-instantiated, - abstract-method, - access-member-before-definition, - anomalous-backslash-in-string, - anomalous-unicode-escape-in-string, - arguments-differ, - assert-on-tuple, - assigning-non-slot, - assignment-from-no-return, - assignment-from-none, - attribute-defined-outside-init, - bad-except-order, - bad-format-character, - bad-format-string-key, - bad-format-string, - bad-open-mode, - bad-reversed-sequence, - bad-staticmethod-argument, - bad-str-strip-call, - bad-super-call, - binary-op-exception, - boolean-datetime, - catching-non-exception, - cell-var-from-loop, - confusing-with-statement, - continue-in-finally, - dangerous-default-value, - duplicate-argument-name, - duplicate-bases, - duplicate-except, - duplicate-key, - expression-not-assigned, - format-combined-specification, - format-needs-mapping, - function-redefined, - global-variable-undefined, - import-error, - import-self, - inconsistent-mro, - inherit-non-class, - init-is-generator, - invalid-all-object, - invalid-format-index, - invalid-length-returned, - invalid-sequence-index, - invalid-slice-index, - invalid-slots-object, - invalid-slots, - invalid-unary-operand-type, - logging-too-few-args, - logging-too-many-args, - logging-unsupported-format, - lost-exception, - method-hidden, - misplaced-bare-raise, - misplaced-future, - missing-format-argument-key, - missing-format-attribute, - missing-format-string-key, - no-member, - no-method-argument, - no-name-in-module, - no-self-argument, - no-value-for-parameter, - non-iterator-returned, - non-parent-method-called, - nonexistent-operator, - not-a-mapping, - not-an-iterable, - not-callable, - not-context-manager, - not-in-loop, - pointless-statement, - pointless-string-statement, - raising-bad-type, - raising-non-exception, - redefined-builtin, - redefined-outer-name, - redundant-keyword-arg, - repeated-keyword, - return-arg-in-generator, - return-in-init, - return-outside-function, - signature-differs, - super-init-not-called, - super-method-not-called, - syntax-error, - test-inherits-tests, - too-few-format-args, - too-many-format-args, - too-many-function-args, - translation-of-non-string, - truncated-format-string, - undefined-all-variable, - undefined-loop-variable, - undefined-variable, - unexpected-keyword-arg, - unexpected-special-method-signature, - unpacking-non-sequence, - unreachable, - unsubscriptable-object, - unsupported-binary-operation, - unsupported-membership-test, - unused-format-string-argument, - unused-format-string-key, - used-before-assignment, - using-constant-test, - yield-outside-function, - - astroid-error, - fatal, - method-check-failed, - parse-error, - raw-checker-failed, - - empty-docstring, - invalid-characters-in-docstring, - missing-docstring, - wrong-spelling-in-comment, - wrong-spelling-in-docstring, - - unused-argument, - unused-import, - unused-variable, - - eval-used, - exec-used, - - bad-classmethod-argument, - bad-mcs-classmethod-argument, - bad-mcs-method-argument, - bare-except, - broad-except, - consider-iterating-dictionary, - consider-using-enumerate, - global-at-module-level, - global-variable-not-assigned, - literal-used-as-attribute, - logging-format-interpolation, - logging-not-lazy, - multiple-imports, - multiple-statements, - no-classmethod-decorator, - no-staticmethod-decorator, - protected-access, - redundant-unittest-assert, - reimported, - simplifiable-if-statement, - simplifiable-range, - singleton-comparison, - superfluous-parens, - unidiomatic-typecheck, - unnecessary-lambda, - unnecessary-pass, - unnecessary-semicolon, - unneeded-not, - useless-else-on-loop, - wrong-assert-type, - - deprecated-method, - deprecated-module, - - too-many-boolean-expressions, - too-many-nested-blocks, - too-many-statements, - - wildcard-import, - wrong-import-order, - wrong-import-position, - - missing-final-newline, - mixed-line-endings, - trailing-newlines, - trailing-whitespace, - unexpected-line-ending-format, - - bad-inline-option, - bad-option-value, - deprecated-pragma, - unrecognized-inline-option, - useless-suppression, -disable = - bad-indentation, - broad-exception-raised, - consider-using-f-string, - duplicate-code, - file-ignored, - fixme, - global-statement, - invalid-name, - locally-disabled, - no-else-return, - suppressed-message, - too-few-public-methods, - too-many-ancestors, - too-many-arguments, - too-many-branches, - too-many-instance-attributes, - too-many-lines, - too-many-locals, - too-many-public-methods, - too-many-return-statements, - ungrouped-imports, - unspecified-encoding, - unused-wildcard-import, - use-maxsplit-arg, - - feature-toggle-needs-doc, - illegal-waffle-usage, - - logging-fstring-interpolation, - invalid-name, - django-not-configured, - consider-using-with, - bad-option-value, - -[REPORTS] -output-format = text -reports = no -score = no - -[BASIC] -module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ -const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ -class-rgx = [A-Z_][a-zA-Z0-9]+$ -function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$ -method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ -attr-rgx = [a-z_][a-z0-9_]{2,30}$ -argument-rgx = [a-z_][a-z0-9_]{2,30}$ -variable-rgx = [a-z_][a-z0-9_]{2,30}$ -class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ -good-names = f,i,j,k,db,ex,Run,_,__ -bad-names = foo,bar,baz,toto,tutu,tata -no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ -docstring-min-length = 5 - -[FORMAT] -max-line-length = 120 -ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ -single-line-if-stmt = no -max-module-lines = 1000 -indent-string = ' ' - -[MISCELLANEOUS] -notes = FIXME,XXX,TODO - -[SIMILARITIES] -min-similarity-lines = 4 -ignore-comments = yes -ignore-docstrings = yes -ignore-imports = no - -[TYPECHECK] -ignore-mixin-members = yes -ignored-classes = SQLObject -unsafe-load-any-extension = yes -generated-members = - REQUEST, - acl_users, - aq_parent, - objects, - DoesNotExist, - can_read, - can_write, - get_url, - size, - content, - status_code, - create, - build, - fields, - tag, - org, - course, - category, - name, - revision, - _meta, - -[VARIABLES] -init-import = no -dummy-variables-rgx = _|dummy|unused|.*_unused -additional-builtins = - -[CLASSES] -defining-attr-methods = __init__,__new__,setUp -valid-classmethod-first-arg = cls -valid-metaclass-classmethod-first-arg = mcs - -[DESIGN] -max-args = 5 -ignored-argument-names = _.* -max-locals = 15 -max-returns = 6 -max-branches = 12 -max-statements = 50 -max-parents = 7 -max-attributes = 7 -min-public-methods = 2 -max-public-methods = 20 - -[IMPORTS] -deprecated-modules = regsub,TERMIOS,Bastion,rexec -import-graph = -ext-import-graph = -int-import-graph = - -[EXCEPTIONS] -overgeneral-exceptions = builtins.Exception - -# f9938a0048db870de9b3db6ba3d2a79f5c48d553 diff --git a/pylintrc_tweaks b/pylintrc_tweaks deleted file mode 100644 index 7b6eb35..0000000 --- a/pylintrc_tweaks +++ /dev/null @@ -1,11 +0,0 @@ -# pylintrc tweaks for use with edx_lint. -[MASTER] -ignore = migrations -load-plugins = edx_lint.pylint,pylint_django,pylint_celery - -[MESSAGES CONTROL] -disable+= - invalid-name, - django-not-configured, - consider-using-with, - bad-option-value, diff --git a/pyproject.toml b/pyproject.toml index 852eab2..f72d03d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,137 @@ +[tool.coverage.run] +include = ['openedx_certificates/**'] +omit = ['*/migrations/*', 'tests/*'] +plugins = ['django_coverage_plugin'] + +[tool.ruff] +line-length = 120 +exclude = ['migrations', 'docs', 'manage.py'] +select = [ + 'F', # Pyflakes + 'E', # Pycodestyle (errors) + 'W', # Pycodestyle (warnings) + 'C90', # mccabe + 'I', # isort + 'N', # pep8-naming + 'D', # pydocstyle + 'UP', # pyupgrade + 'YTT', # flake8-2020 + 'ANN', # flake8-annotations + 'ASYNC',# flake8-async + # 'TRIO', # flake8-trio + 'S', # flake8-bandit + 'BLE', # flake8-blind-except + 'FBT', # flake8-boolean-trap + 'B', # flake8-bugbear + 'A', # flake8-builtins + 'COM', # flake8-commas + # 'CPY', # flake8-copyright + 'C4', # flake8-comprehensions + 'DTZ', # flake8-datetimez + 'T10', # flake8-debugger + 'DJ', # flake8-django + 'EM', # flake8-errmsg + 'EXE', # flake8-executable + 'FA', # flake8-future-annotations + 'ISC', # flake8-implicit-str-concat + 'ICN', # flake8-import-conventions + 'G', # flake8-logging-format + 'INP', # flake8-no-pep420 + 'PIE', # flake8-pie + 'T20', # flake8-print + 'PYI', # flake8-pyi + 'PT', # flake8-pytest-style + 'Q', # flake8-quotes + 'RSE', # flake8-raise + 'RET', # flake8-return + 'SLF', # flake8-self + 'SLOT', # flake8-slots + 'SIM', # flake8-simplify + 'TID', # flake8-tidy-imports + 'TCH', # flake8-type-checking + 'INT', # flake8-gettext + 'ARG', # flake8-unused-arguments + 'PTH', # flake8-use-pathlib + 'TD', # flake8-todos + # 'FIX', # flake8-fixme + 'ERA', # eradicate + 'PD', # pandas-vet + 'PGH', # pygrep-hooks + 'PL', # Pylint + 'TRY', # tryceratops + 'FLY', # flynt + 'NPY', # NumPy-specific rules + 'AIR', # Airflow + 'PERF', # Perflint + # 'FURB', # refurb + # 'LOG', # flake8-logging + 'RUF', # Ruff-specific rules +] +ignore = [ + 'ANN002', # missing-type-args + 'ANN003', # missing-type-kwargs + 'ANN101', # missing-type-self + 'ANN102', # missing-type-cls + 'ANN204', # missing-return-type-special-method + 'D203', # one-blank-line-before-class + 'D212', # multi-line-summary-first-line (incompatible with D213) + 'Q000', # bad-quotes-inline-string + 'TD002', # missing-t\odo-author + 'TD003', # missing-t\odo-link + 'TRY003', # raise-vanilla-args +] +target-version = 'py38' + +[tool.ruff.per-file-ignores] +'tests/*' = [ + 'S101', # assert + 'ANN205', # missing-return-type-static-method + 'INP001', # implicit-namespace-package + 'SLF001', # private-member-access +] + +[tool.ruff.flake8-annotations] +suppress-none-returning = true + +[tool.ruff.flake8-builtins] +builtins-ignorelist = ['list'] + +# Consider instead of Q000. +#[tool.ruff.flake8-quotes] +#inline-quotes = 'single' + +[tool.ruff.pydocstyle] +convention = "google" + +[tool.ruff.pylint] +allow-magic-value-types = ['int', 'str'] + [tool.black] line-length = 120 target-version = ['py38'] skip_string_normalization = true +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | migrations +)/ +''' -[tool.ruff] -line-length = 120 +[tool.pytest.ini_options] +filterwarnings = [ + # https://github.com/openedx/completion/pull/259 + "ignore:'completion' defines default_app_config:django.utils.deprecation.RemovedInDjango41Warning", + "ignore:pkg_resources is deprecated as an API:DeprecationWarning", + "ignore:Deprecated call to `pkg_resources.declare_namespace.*sphinxcontrib:DeprecationWarning", +] +DJANGO_SETTINGS_MODULE = "test_settings" +addopts = "--cov openedx_certificates --cov tests --cov-report term-missing --cov-report xml" +norecursedirs = ".* docs requirements site-packages" diff --git a/requirements/dev.txt b/requirements/dev.txt index cbe408b..7b64617 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,11 +8,6 @@ asgiref==3.7.2 # via # -r requirements/quality.txt # django -astroid==2.15.6 - # via - # -r requirements/quality.txt - # pylint - # pylint-celery black==23.7.0 # via -r requirements/quality.txt build==0.10.0 @@ -26,28 +21,17 @@ click==8.1.7 # -r requirements/pip-tools.txt # -r requirements/quality.txt # black - # click-log # code-annotations - # edx-lint # pip-tools -click-log==0.4.0 - # via - # -r requirements/quality.txt - # edx-lint code-annotations==1.5.0 - # via - # -r requirements/quality.txt - # edx-lint + # via -r requirements/quality.txt coverage[toml]==7.3.0 # via # -r requirements/quality.txt + # django-coverage-plugin # pytest-cov diff-cover==7.7.0 # via -r requirements/dev.in -dill==0.3.7 - # via - # -r requirements/quality.txt - # pylint distlib==0.3.7 # via # -r requirements/ci.txt @@ -58,12 +42,12 @@ django==3.2.20 # -r requirements/quality.txt # django-model-utils # edx-i18n-tools +django-coverage-plugin==3.1.0 + # via -r requirements/quality.txt django-model-utils==4.3.1 # via -r requirements/quality.txt edx-i18n-tools==1.1.0 # via -r requirements/dev.in -edx-lint==5.3.4 - # via -r requirements/quality.txt exceptiongroup==1.1.3 # via # -r requirements/quality.txt @@ -77,27 +61,15 @@ iniconfig==2.0.0 # via # -r requirements/quality.txt # pytest -isort==5.12.0 - # via - # -r requirements/quality.txt - # pylint jinja2==3.1.2 # via # -r requirements/quality.txt # code-annotations # diff-cover -lazy-object-proxy==1.9.0 - # via - # -r requirements/quality.txt - # astroid markupsafe==2.1.3 # via # -r requirements/quality.txt # jinja2 -mccabe==0.7.0 - # via - # -r requirements/quality.txt - # pylint mypy-extensions==1.0.0 # via # -r requirements/quality.txt @@ -128,7 +100,6 @@ platformdirs==3.10.0 # -r requirements/ci.txt # -r requirements/quality.txt # black - # pylint # virtualenv pluggy==1.2.0 # via @@ -143,32 +114,8 @@ py==1.11.0 # via # -r requirements/ci.txt # tox -pycodestyle==2.11.0 - # via -r requirements/quality.txt -pydocstyle==6.3.0 - # via -r requirements/quality.txt pygments==2.16.1 # via diff-cover -pylint==2.17.5 - # via - # -r requirements/quality.txt - # edx-lint - # pylint-celery - # pylint-django - # pylint-plugin-utils -pylint-celery==0.3 - # via - # -r requirements/quality.txt - # edx-lint -pylint-django==2.5.3 - # via - # -r requirements/quality.txt - # edx-lint -pylint-plugin-utils==0.8.2 - # via - # -r requirements/quality.txt - # pylint-celery - # pylint-django pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt @@ -195,18 +142,12 @@ pyyaml==6.0.1 # -r requirements/quality.txt # code-annotations # edx-i18n-tools -ruff==0.0.285 +ruff==0.0.286 # via -r requirements/quality.txt six==1.16.0 # via # -r requirements/ci.txt - # -r requirements/quality.txt - # edx-lint # tox -snowballstemmer==2.2.0 - # via - # -r requirements/quality.txt - # pydocstyle sqlparse==0.4.4 # via # -r requirements/quality.txt @@ -228,14 +169,9 @@ tomli==2.0.1 # build # coverage # pip-tools - # pylint # pyproject-hooks # pytest # tox -tomlkit==0.12.1 - # via - # -r requirements/quality.txt - # pylint tox==3.28.0 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt @@ -247,9 +183,7 @@ typing-extensions==4.7.1 # via # -r requirements/quality.txt # asgiref - # astroid # black - # pylint virtualenv==20.24.3 # via # -r requirements/ci.txt @@ -258,10 +192,6 @@ wheel==0.41.2 # via # -r requirements/pip-tools.txt # pip-tools -wrapt==1.15.0 - # via - # -r requirements/quality.txt - # astroid # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/doc.txt b/requirements/doc.txt index 085e524..ff373de 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -37,6 +37,7 @@ code-annotations==1.5.0 coverage[toml]==7.3.0 # via # -r requirements/test.txt + # django-coverage-plugin # pytest-cov cryptography==41.0.3 # via secretstorage @@ -45,6 +46,8 @@ django==3.2.20 # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt # django-model-utils +django-coverage-plugin==3.1.0 + # via -r requirements/test.txt django-model-utils==4.3.1 # via -r requirements/test.txt doc8==1.1.1 diff --git a/requirements/quality.in b/requirements/quality.in index 74b416f..ca73948 100644 --- a/requirements/quality.in +++ b/requirements/quality.in @@ -4,9 +4,5 @@ -r test.txt # Core and testing dependencies for this package -edx-lint # edX pylint rules and plugins -isort # to standardize order of imports -pycodestyle # PEP 8 compliance validation -pydocstyle # PEP 257 compliance validation black # Code formatter ruff # Linter diff --git a/requirements/quality.txt b/requirements/quality.txt index c09c086..36c1f9c 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -8,40 +8,29 @@ asgiref==3.7.2 # via # -r requirements/test.txt # django -astroid==2.15.6 - # via - # pylint - # pylint-celery black==23.7.0 # via -r requirements/quality.in click==8.1.7 # via # -r requirements/test.txt # black - # click-log # code-annotations - # edx-lint -click-log==0.4.0 - # via edx-lint code-annotations==1.5.0 - # via - # -r requirements/test.txt - # edx-lint + # via -r requirements/test.txt coverage[toml]==7.3.0 # via # -r requirements/test.txt + # django-coverage-plugin # pytest-cov -dill==0.3.7 - # via pylint django==3.2.20 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt # django-model-utils +django-coverage-plugin==3.1.0 + # via -r requirements/test.txt django-model-utils==4.3.1 # via -r requirements/test.txt -edx-lint==5.3.4 - # via -r requirements/quality.in exceptiongroup==1.1.3 # via # -r requirements/test.txt @@ -50,22 +39,14 @@ iniconfig==2.0.0 # via # -r requirements/test.txt # pytest -isort==5.12.0 - # via - # -r requirements/quality.in - # pylint jinja2==3.1.2 # via # -r requirements/test.txt # code-annotations -lazy-object-proxy==1.9.0 - # via astroid markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 -mccabe==0.7.0 - # via pylint mypy-extensions==1.0.0 # via black packaging==23.1 @@ -80,31 +61,11 @@ pbr==5.11.1 # -r requirements/test.txt # stevedore platformdirs==3.10.0 - # via - # black - # pylint + # via black pluggy==1.2.0 # via # -r requirements/test.txt # pytest -pycodestyle==2.11.0 - # via -r requirements/quality.in -pydocstyle==6.3.0 - # via -r requirements/quality.in -pylint==2.17.5 - # via - # edx-lint - # pylint-celery - # pylint-django - # pylint-plugin-utils -pylint-celery==0.3 - # via edx-lint -pylint-django==2.5.3 - # via edx-lint -pylint-plugin-utils==0.8.2 - # via - # pylint-celery - # pylint-django pytest==7.4.0 # via # -r requirements/test.txt @@ -126,12 +87,8 @@ pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations -ruff==0.0.285 +ruff==0.0.286 # via -r requirements/quality.in -six==1.16.0 - # via edx-lint -snowballstemmer==2.2.0 - # via pydocstyle sqlparse==0.4.4 # via # -r requirements/test.txt @@ -149,16 +106,9 @@ tomli==2.0.1 # -r requirements/test.txt # black # coverage - # pylint # pytest -tomlkit==0.12.1 - # via pylint typing-extensions==4.7.1 # via # -r requirements/test.txt # asgiref - # astroid # black - # pylint -wrapt==1.15.0 - # via astroid diff --git a/requirements/test.in b/requirements/test.in index 6797160..33ec25e 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -4,5 +4,6 @@ -r base.txt # Core dependencies for this package pytest-cov # pytest extension for code coverage statistics +django-coverage-plugin # https://github.com/nedbat/django_coverage_plugin pytest-django # pytest extension for better Django support code-annotations # provides commands used by the pii_check make target. diff --git a/requirements/test.txt b/requirements/test.txt index 2399e49..8dca551 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -13,11 +13,15 @@ click==8.1.7 code-annotations==1.5.0 # via -r requirements/test.in coverage[toml]==7.3.0 - # via pytest-cov + # via + # django-coverage-plugin + # pytest-cov # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt # django-model-utils +django-coverage-plugin==3.1.0 + # via -r requirements/test.in django-model-utils==4.3.1 # via -r requirements/base.txt exceptiongroup==1.1.3 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d782599..0000000 --- a/setup.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[isort] -include_trailing_comma = True -indent = ' ' -line_length = 120 -multi_line_output = 3 -skip= - migrations - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py index 277b266..28eeaea 100755 --- a/setup.py +++ b/setup.py @@ -1,15 +1,16 @@ #!/usr/bin/env python -""" -Package metadata for openedx_certificates. -""" +"""Package metadata for openedx_certificates.""" +from __future__ import annotations + import os import re import sys +from pathlib import Path from setuptools import find_packages, setup -def get_version(*file_paths): +def get_version(file_path: Path) -> str: """ Extract the version string from the file. @@ -17,15 +18,16 @@ def get_version(*file_paths): - file_paths: relative path fragments to file with version string """ - filename = os.path.join(os.path.dirname(__file__), *file_paths) - version_file = open(filename, encoding="utf8").read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) + filename = Path(__file__).parent / file_path + with Path(filename).open(encoding="utf8") as f: + version_file = f.read() + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) - raise RuntimeError('Unable to find version string.') + raise RuntimeError('Unable to find version string.') # noqa: EM101 -def load_requirements(*requirements_paths): +def load_requirements(*requirements_paths: Path) -> list[str]: # noqa: C901 """ Load all requirements from the specified requirements files. @@ -36,7 +38,7 @@ def load_requirements(*requirements_paths): # e.g. {"django": "Django", "confluent-kafka": "confluent_kafka[avro]"} by_canonical_name = {} - def check_name_consistent(package): + def check_name_consistent(package: str) -> None: """ Raise exception if package is named different ways. @@ -50,9 +52,9 @@ def check_name_consistent(package): if seen_spelling is None: by_canonical_name[canonical] = package elif seen_spelling != package: - raise Exception( - f'Encountered both "{seen_spelling}" and "{package}" in requirements ' - 'and constraints files; please use just one or the other.' + raise Exception( # noqa: TRY002 + f'Encountered both "{seen_spelling}" and "{package}" in requirements ' # noqa: EM102 + 'and constraints files; please use just one or the other.', ) requirements = {} @@ -62,10 +64,14 @@ def check_name_consistent(package): re_package_name_base_chars = r"a-zA-Z0-9\-_." # chars allowed in base package name # Two groups: name[maybe,extras], and optionally a constraint requirement_line_regex = re.compile( - r"([%s]+(?:\[[%s,\s]+\])?)([<>=][^#\s]+)?" % (re_package_name_base_chars, re_package_name_base_chars) + fr"([{re_package_name_base_chars}]+(?:\[[{re_package_name_base_chars},\s]+])?)([<>=][^#\s]+)?", ) - def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present): + def add_version_constraint_or_raise( + current_line: str, + current_requirements: dict[str, str], + add_if_not_present: bool, # noqa: FBT001 + ): regex_match = requirement_line_regex.match(current_line) if regex_match: package = regex_match.group(1) @@ -75,11 +81,12 @@ def add_version_constraint_or_raise(current_line, current_requirements, add_if_n # It's fine to add constraints to an unconstrained package, # but raise an error if there are already constraints in place. if existing_version_constraints and existing_version_constraints != version_constraints: + # noinspection PyExceptionInherit raise BaseException( - f'Multiple constraint definitions found for {package}:' + f'Multiple constraint definitions found for {package}:' # noqa: EM102 f' "{existing_version_constraints}" and "{version_constraints}".' f'Combine constraints into one location with {package}' - f'{existing_version_constraints},{version_constraints}.' + f'{existing_version_constraints},{version_constraints}.', ) if add_if_not_present or package in current_requirements: current_requirements[package] = version_constraints @@ -87,46 +94,44 @@ def add_version_constraint_or_raise(current_line, current_requirements, add_if_n # Read requirements from .in files and store the path to any # constraint files that are pulled in. for path in requirements_paths: - with open(path) as reqs: + with path.open() as reqs: for line in reqs: if is_requirement(line): - add_version_constraint_or_raise(line, requirements, True) + add_version_constraint_or_raise(line, requirements, add_if_not_present=True) if line and line.startswith('-c') and not line.startswith('-c http'): - constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip()) + constraint_files.add(path.parent / line.split('#')[0].replace('-c', '').strip()) # process constraint files: add constraints to existing requirements for constraint_file in constraint_files: - with open(constraint_file) as reader: + with constraint_file.open() as reader: for line in reader: if is_requirement(line): - add_version_constraint_or_raise(line, requirements, False) + add_version_constraint_or_raise(line, requirements, add_if_not_present=False) # process back into list of pkg><=constraints strings - constrained_requirements = [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())] - return constrained_requirements + return [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())] -def is_requirement(line): +def is_requirement(line: str) -> bool: """ Return True if the requirement line is a package requirement. Returns: - bool: True if the line is not blank, a comment, - a URL, or an included file + bool: True if the line is not blank, a comment, a URL, or an included file. """ - return line and line.strip() and not line.startswith(("-r", "#", "-e", "git+", "-c")) + return bool(line and line.strip() and not line.startswith(("-r", "#", "-e", "git+", "-c"))) -VERSION = get_version('openedx_certificates', '__init__.py') +VERSION = get_version(Path('openedx_certificates/__init__.py')) if sys.argv[-1] == 'tag': - print("Tagging the version on github:") - os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) - os.system("git push --tags") + print("Tagging the version on github:") # noqa: T201 + os.system(f"git tag -a {VERSION} -m 'version {VERSION}'") # noqa: S605x + os.system("git push --tags") # noqa: S605, S607 sys.exit() -README = open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding="utf8").read() -CHANGELOG = open(os.path.join(os.path.dirname(__file__), 'CHANGELOG.rst'), encoding="utf8").read() +README = (Path(__file__).parent / 'README.rst').open(encoding="utf8").read() +CHANGELOG = (Path(__file__).parent / 'CHANGELOG.rst').open(encoding="utf8").read() setup( name='openedx-certificates', @@ -142,7 +147,8 @@ def is_requirement(line): exclude=["*tests"], ), include_package_data=True, - install_requires=load_requirements('requirements/base.in'), + install_requires=load_requirements(Path('requirements/base.in')), + options={'bdist_wheel': {'universal': True}}, python_requires=">=3.8", license="AGPL 3.0", zip_safe=False, diff --git a/test_settings.py b/test_settings.py index ea4d027..c00338b 100644 --- a/test_settings.py +++ b/test_settings.py @@ -5,14 +5,12 @@ Django applications, so these settings will not be used. """ -from os.path import abspath, dirname, join +from pathlib import Path -def root(*args): - """ - Get the absolute path of the given path relative to the project root. - """ - return join(abspath(dirname(__file__)), *args) +def root(path: Path) -> Path: + """Get the absolute path of the given path relative to the project root.""" + return Path(__file__).parent.resolve() / path DATABASES = { @@ -23,7 +21,7 @@ def root(*args): 'PASSWORD': '', 'HOST': '', 'PORT': '', - } + }, } INSTALLED_APPS = ( @@ -36,12 +34,12 @@ def root(*args): ) LOCALE_PATHS = [ - root('openedx_certificates', 'conf', 'locale'), + root(Path('openedx_certificates/conf/locale')), ] ROOT_URLCONF = 'openedx_certificates.urls' -SECRET_KEY = 'insecure-secret-key' +SECRET_KEY = 'insecure-secret-key' # noqa: S105 MIDDLEWARE = ( 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -59,5 +57,5 @@ def root(*args): 'django.contrib.messages.context_processors.messages', # this is required for admin ], }, - } + }, ] diff --git a/tests/test_models.py b/tests/test_models.py index c366a62..834ad4d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,30 +1,19 @@ -#!/usr/bin/env python -""" -Tests for the `openedx-certificates` models module. -""" +"""Tests for the `openedx-certificates` models module.""" import pytest class TestExternalCertificateConfiguration: - """ - Tests of the ExternalCertificateConfiguration model. - """ + """Tests of the ExternalCertificateConfiguration model.""" @pytest.mark.skip(reason="Placeholder to allow pytest to succeed before real tests are in place.") def test_placeholder(self): - """ - TODO: Delete this test once there are real tests. - """ + """TODO: Delete this test once there are real tests.""" class TestExternalCertificateType: - """ - Tests of the ExternalCertificateType model. - """ + """Tests of the ExternalCertificateType model.""" @pytest.mark.skip(reason="Placeholder to allow pytest to succeed before real tests are in place.") def test_placeholder(self): - """ - TODO: Delete this test once there are real tests. - """ + """TODO: Delete this test once there are real tests.""" diff --git a/tox.ini b/tox.ini index fb40586..f8f9616 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38-django{32,40} +envlist = py38-django{32,40},docs,quality,pii_check,package [doc8] ; D001 = Line too long @@ -29,11 +29,6 @@ max-line-length = 120 ignore = D101,D200,D203,D212,D215,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414 match-dir = (?!migrations) -[pytest] -DJANGO_SETTINGS_MODULE = test_settings -addopts = --cov openedx_certificates --cov tests --cov-report term-missing --cov-report xml -norecursedirs = .* docs requirements site-packages - [testenv] deps = django32: Django>=3.2,<4.0 @@ -47,7 +42,7 @@ commands = setenv = DJANGO_SETTINGS_MODULE = test_settings PYTHONPATH = {toxinidir} - # Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by shpinx. + # Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by sphinx. SPHINXOPTS = -W whitelist_externals = make @@ -66,17 +61,10 @@ commands = [testenv:quality] whitelist_externals = make - rm - touch deps = -r{toxinidir}/requirements/quality.txt commands = - touch tests/__init__.py - pylint openedx_certificates tests test_utils manage.py setup.py - rm tests/__init__.py - pycodestyle openedx_certificates tests manage.py setup.py - pydocstyle openedx_certificates tests manage.py setup.py - isort --check-only --diff tests test_utils openedx_certificates manage.py setup.py test_settings.py + ruff . make selfcheck [testenv:pii_check] From bb521422c38930aa18bc35deb6f238705295493c Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Sun, 27 Aug 2023 20:57:17 +0200 Subject: [PATCH 02/46] style: add yamllint --- .annotation_safe_list.yml | 32 ++++++------ .github/workflows/ci.yml | 50 +++++++++---------- .github/workflows/lint.yml | 2 +- .../workflows/upgrade-python-requirements.yml | 2 +- .pii_annotations.yml | 44 ++++++++-------- .yamllint.yml | 22 ++++++++ Makefile | 3 +- catalog-info.yaml | 12 ++--- requirements/dev.txt | 4 ++ requirements/quality.in | 1 + requirements/quality.txt | 7 ++- tox.ini | 1 + 12 files changed, 107 insertions(+), 73 deletions(-) create mode 100644 .yamllint.yml diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml index 62eaaa7..e4eb085 100644 --- a/.annotation_safe_list.yml +++ b/.annotation_safe_list.yml @@ -8,34 +8,34 @@ # ".. choice_annotation:": foo, bar, baz admin.LogEntry: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII auth.Group: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII auth.Permission: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII auth.User: - ".. pii": "This model minimally contains a username, password, and email" - ".. pii_types": "username, email_address, password" - ".. pii_retirement": "consumer_api" + ".. pii": This model minimally contains a username, password, and email + ".. pii_types": username, email_address, password + ".. pii_retirement": consumer_api contenttypes.ContentType: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII sessions.Session: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII social_django.Association: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII social_django.Code: - ".. pii:": "Email address" + ".. pii:": Email address ".. pii_types:": other ".. pii_retirement:": local_api social_django.Nonce: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII social_django.Partial: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII social_django.UserSocialAuth: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII waffle.Flag: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII waffle.Sample: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII waffle.Switch: - ".. no_pii:": "This model has no PII" + ".. no_pii:": This model has no PII diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c62dc88..2c428cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: branches: [main] pull_request: branches: - - '**' + - '**' jobs: @@ -19,27 +19,27 @@ jobs: toxenv: [quality, docs, pii_check, django32, django40, package] steps: - - uses: actions/checkout@v3 - - name: setup python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install pip - run: pip install -r requirements/pip.txt - - - name: Install Dependencies - run: pip install -r requirements/ci.txt - - - name: Run Tests - env: - TOXENV: ${{ matrix.toxenv }} - run: tox - - - name: Run coverage - if: matrix.python-version == '3.8' && matrix.toxenv == 'django32' - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - flags: unittests - fail_ci_if_error: true + - uses: actions/checkout@v3 + - name: setup python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install pip + run: pip install -r requirements/pip.txt + + - name: Install Dependencies + run: pip install -r requirements/ci.txt + + - name: Run Tests + env: + TOXENV: ${{ matrix.toxenv }} + run: tox + + - name: Run coverage + if: matrix.python-version == '3.8' && matrix.toxenv == 'django32' + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + fail_ci_if_error: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b3d2dbe..8e36925 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,7 +5,7 @@ on: branches: [main] pull_request: branches: - - '**' + - '**' jobs: lint: diff --git a/.github/workflows/upgrade-python-requirements.yml b/.github/workflows/upgrade-python-requirements.yml index 61bfc00..84125bb 100644 --- a/.github/workflows/upgrade-python-requirements.yml +++ b/.github/workflows/upgrade-python-requirements.yml @@ -2,7 +2,7 @@ name: Upgrade Python Requirements on: schedule: - - cron: "0 2 * * 1" + - cron: 0 2 * * 1 workflow_dispatch: inputs: branch: diff --git a/.pii_annotations.yml b/.pii_annotations.yml index 7da8f3c..d224255 100644 --- a/.pii_annotations.yml +++ b/.pii_annotations.yml @@ -7,29 +7,29 @@ annotations: "pii_group": - ".. pii:": - ".. pii_types:": - choices: - - id # Unique identifier for the user which is shared across systems - - name # Used for any part of the user's name - - username - - password - - location # Used for any part of any type address or country stored - - phone_number # Used for phone or fax numbers - - email_address - - birth_date # Used for any part of a stored birth date - - ip # IP address - - external_service # Used for external service ids or links such as social media links or usernames, website links, etc. - - biography # Any type of free-form biography field - - gender - - sex - - image - - video - - other + choices: + - id # Unique identifier for the user which is shared across systems + - name # Used for any part of the user's name + - username + - password + - location # Used for any part of any type address or country stored + - phone_number # Used for phone or fax numbers + - email_address + - birth_date # Used for any part of a stored birth date + - ip # IP address + - external_service # Used for external service ids or links such as social media links or usernames, website links, etc. + - biography # Any type of free-form biography field + - gender + - sex + - image + - video + - other - ".. pii_retirement:": - choices: - - retained # Intentionally kept for legal reasons - - local_api # An API exists in this repository for retiring this information - - consumer_api # The data's consumer must implement an API for retiring this information - - third_party # A third party API exists to retire this data + choices: + - retained # Intentionally kept for legal reasons + - local_api # An API exists in this repository for retiring this information + - consumer_api # The data's consumer must implement an API for retiring this information + - third_party # A third party API exists to retire this data extensions: python: - py diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..21d1d1e --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,22 @@ +extends: default + +ignore: | + .pytest_cache/ + .ruff_cache/ + .tox/ + pii_report/ + venv/ +rules: + indentation: + spaces: consistent + check-multi-line-strings: true + line-length: disable + document-start: disable + quoted-strings: + quote-type: any + required: only-when-needed + braces: + forbid: non-empty + level: warning + truthy: + check-keys: false diff --git a/Makefile b/Makefile index f5bffe9..95b6ac5 100644 --- a/Makefile +++ b/Makefile @@ -63,8 +63,9 @@ piptools: ## install pinned version of pip-compile and pip-sync requirements: piptools ## install development environment requirements pip-sync -q requirements/dev.txt requirements/private.* -lint: ## lint all Python files +lint: ## lint all files black . + yamllint . test: clean ## run tests in the current virtualenv pytest diff --git a/catalog-info.yaml b/catalog-info.yaml index 4b1a33c..9f51a02 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -4,8 +4,8 @@ apiVersion: backstage.io/v1alpha1 kind: "" metadata: - name: 'openedx-certificates' - description: "A pluggable service for preparing Open edX certificates." + name: openedx-certificates + description: A pluggable service for preparing Open edX certificates. annotations: # (Optional) Annotation keys and values can be whatever you want. # We use it in Open edX repos to have a comma-separated list of GitHub user @@ -21,12 +21,12 @@ spec: type: '' # (Required) Acceptable Lifecycle Values: experimental, production, deprecated - lifecycle: 'experimental' + lifecycle: experimental # (Optional) The value can be the name of any known component. - subcomponentOf: '' + subcomponentOf: # (Optional) An array of different components or resources. dependsOn: - - '' - - '' + - + - diff --git a/requirements/dev.txt b/requirements/dev.txt index 7b64617..7b9b149 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -89,6 +89,7 @@ pathspec==0.11.2 # via # -r requirements/quality.txt # black + # yamllint pbr==5.11.1 # via # -r requirements/quality.txt @@ -142,6 +143,7 @@ pyyaml==6.0.1 # -r requirements/quality.txt # code-annotations # edx-i18n-tools + # yamllint ruff==0.0.286 # via -r requirements/quality.txt six==1.16.0 @@ -192,6 +194,8 @@ wheel==0.41.2 # via # -r requirements/pip-tools.txt # pip-tools +yamllint==1.32.0 + # via -r requirements/quality.txt # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/quality.in b/requirements/quality.in index ca73948..80413cb 100644 --- a/requirements/quality.in +++ b/requirements/quality.in @@ -6,3 +6,4 @@ black # Code formatter ruff # Linter +yamllint # A linter for YAML files. diff --git a/requirements/quality.txt b/requirements/quality.txt index 36c1f9c..991f75f 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -55,7 +55,9 @@ packaging==23.1 # black # pytest pathspec==0.11.2 - # via black + # via + # black + # yamllint pbr==5.11.1 # via # -r requirements/test.txt @@ -87,6 +89,7 @@ pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # yamllint ruff==0.0.286 # via -r requirements/quality.in sqlparse==0.4.4 @@ -112,3 +115,5 @@ typing-extensions==4.7.1 # -r requirements/test.txt # asgiref # black +yamllint==1.32.0 + # via -r requirements/quality.in diff --git a/tox.ini b/tox.ini index f8f9616..70f04a0 100644 --- a/tox.ini +++ b/tox.ini @@ -65,6 +65,7 @@ deps = -r{toxinidir}/requirements/quality.txt commands = ruff . + yamllint --strict --format parsable . make selfcheck [testenv:pii_check] From 515e647286fb5f557fb719f5af98f9a7c5aaedab Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 24 Oct 2023 23:54:17 +0200 Subject: [PATCH 03/46] docs: create ADRs --- .editorconfig | 3 - .github/workflows/ci.yml | 4 + .readthedocs.yml | 2 + README.rst | 97 ++------------- docs/conf.py | 1 + docs/decisions/0001-purpose-of-this-repo.rst | 115 +++++++++++++++--- docs/decisions/0002-architecture.rst | 119 +++++++++++++++++++ docs/getting_started.rst | 58 ++++++++- 8 files changed, 285 insertions(+), 114 deletions(-) create mode 100644 docs/decisions/0002-architecture.rst diff --git a/.editorconfig b/.editorconfig index 5cf5aed..789b285 100644 --- a/.editorconfig +++ b/.editorconfig @@ -94,7 +94,4 @@ trim_trailing_whitespace = false [COMMIT_EDITMSG] max_line_length = 72 -[*.rst] -max_line_length = 79 - # f2f02689fced7a2e0c62c2f9803184114dc2ae4b diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c428cf..9d6aaad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,10 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: setup graphviz + if: matrix.toxenv == 'docs' + uses: ts-graphviz/setup-graphviz@v1 + - name: Install pip run: pip install -r requirements/pip.txt diff --git a/.readthedocs.yml b/.readthedocs.yml index e273e92..a9b8da6 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,6 +13,8 @@ build: os: ubuntu-22.04 tools: python: "3.8" + apt_packages: + - graphviz python: install: diff --git a/README.rst b/README.rst index 23b2a15..1a98024 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,5 @@ openedx-certificates -############################# - -.. note:: - - This README was auto-generated. Maintainer: please review its contents and - update all relevant sections. Instructions to you are marked with - "PLACEHOLDER" or "TODO". Update or remove those sections, and remove this - note when you are done. +#################### |pypi-badge| |ci-badge| |codecov-badge| |doc-badge| |pyversions-badge| |license-badge| |status-badge| @@ -16,92 +9,16 @@ Purpose A pluggable service for preparing Open edX certificates. -TODO: The ``README.rst`` file should start with a brief description of the repository and its purpose. -It should be described in the context of other repositories under the ``openedx`` -organization. It should make clear where this fits in to the overall Open edX -codebase and should be oriented towards people who are new to the Open edX -project. - -Getting Started -*************** - -Developing -========== - -One Time Setup --------------- -.. code-block:: - - # Clone the repository - git clone git@github.com:open-craft/openedx-certificates.git - cd openedx-certificates - - # Set up a virtualenv with the same name as the repo and activate it - # Here's how you might do that if you have virtualenvwrapper setup. - mkvirtualenv -p python3.8 openedx-certificates - - -Every time you develop something in this repo ---------------------------------------------- -.. code-block:: - - # Activate the virtualenv - # Here's how you might do that if you're using virtualenvwrapper. - workon openedx-certificates - - # Grab the latest code - git checkout main - git pull - - # Install/update the dev requirements - make requirements - - # Run the tests and quality checks (to verify the status before you make any changes) - make validate - - # Make a new branch for your changes - git checkout -b / - - # Using your favorite editor, edit the code to make your change. - vim ... - - # Run your new tests - pytest ./path/to/new/tests - - # Run all the tests and quality checks - make validate - - # Commit all your changes - git commit ... - git push - - # Open a PR and ask for review. - -Deploying -========= - -TODO: How can a new user go about deploying this component? Is it just a few -commands? Is there a larger how-to that should be linked here? +Documentation +************* -PLACEHOLDER: For details on how to deploy this component, see the `deployment how-to`_ +Start by going through `the documentation`_. -.. _deployment how-to: https://docs.openedx.org/projects/openedx-certificates/how-tos/how-to-deploy-this-component.html +.. _the documentation: https://openedx-certificates.readthedocs.io/en/latest Getting Help ************ -Documentation -============= - -PLACEHOLDER: Start by going through `the documentation`_. If you need more help see below. - -.. _the documentation: https://docs.openedx.org/projects/openedx-certificates - -(TODO: `Set up documentation `_) - -More Help -========= - If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in the community. @@ -151,6 +68,8 @@ All community members are expected to follow the `Open edX Code of Conduct`_. People ****** +.. TODO: Add the maintainers. + The assigned maintainers for this component and other project details may be found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml`` file in this repo. @@ -175,7 +94,7 @@ Please do not report security issues in public. Please email security@openedx.or :alt: Codecov .. |doc-badge| image:: https://readthedocs.org/projects/openedx-certificates/badge/?version=latest - :target: https://docs.openedx.org/projects/openedx-certificates + :target: https://openedx-certificates.readthedocs.io/en/latest :alt: Documentation .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/openedx-certificates.svg diff --git a/docs/conf.py b/docs/conf.py index 70cc4c4..8049003 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,6 +67,7 @@ def get_version(*file_paths): 'sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', 'sphinx.ext.napoleon', + 'sphinx.ext.graphviz', ] # A list of warning types to suppress arbitrary warning messages. diff --git a/docs/decisions/0001-purpose-of-this-repo.rst b/docs/decisions/0001-purpose-of-this-repo.rst index d67cb89..fb05eb5 100644 --- a/docs/decisions/0001-purpose-of-this-repo.rst +++ b/docs/decisions/0001-purpose-of-this-repo.rst @@ -4,54 +4,133 @@ Status ****** -**Draft** - -.. TODO: When ready, update the status from Draft to Provisional or Accepted. +**Accepted** .. Standard statuses + - **Draft** if the decision is newly proposed and in active discussion - **Provisional** if the decision is still preliminary and in experimental phase - **Accepted** *(date)* once it is agreed upon - **Superseded** *(date)* with a reference to its replacement if a later ADR changes or reverses the decision - If an ADR has Draft status and the PR is under review, you can either use the intended final status (e.g. Provisional, Accepted, etc.), or you can clarify both the current and intended status using something like the following: "Draft (=> Provisional)". Either of these options is especially useful if the merged status is not intended to be Accepted. + If an ADR has Draft status and the PR is under review, you can either use the intended final status + (e.g. Provisional, Accepted, etc.), or you can clarify both the current and intended status using something like the + following: "Draft (=> Provisional)". Either of these options is especially useful if the merged status is not + intended to be Accepted. Context ******* -TODO: Add context of what led to the creation of this repo. +We want to issue the certificates and badges for students participating in the courses. + +The present workflow used for the certificates is quite complex because we generate them based on the data pulled from +the Open edX databases (MySQL and MongoDB). The purpose of this repository is to implement a service closely connected +with Open edX. + +We want to support the following certificate types: + +#. Certificate of achievement + We grant it when a student receives a passing grade in both of the following: + + #. Course assignments (excluding the final exam). + #. The final exam. + +#. Certificate of completion + We grant it when a student receives the required percentage of the completion checkmarks in the course. + +#. Badge of achievement/completion (temporary name) + Almost identical to the certificates of achievement/completion. The only difference is that we award them for + completing :ref:`Lessons `. + +#. Pathway certificate + We grant it when a student receives a passing grade in all courses/lessons in the :ref:`Pathway `. + +#. Achievement (temporary name) + Similar to the `Badges`_, recently removed from Open edX. Example criteria: + + #. A student received a passing grade on the course assignments (excluding the final exam). + #. A student received a passing grade on the final exam. + #. A student received all completion checkmarks in the course. + #. A student has posted a comment in the forum. + + +Other notes: -.. This section describes the forces at play, including technological, political, social, and project local. These forces are probably in tension, and should be called out as such. The language in this section is value-neutral. It is simply describing facts. +#. We do not need to pull data in real time. This service can retrieve data periodically, but the frequency should be + configurable per course. +#. We need to design an interface for configuring these certificates per course. The goal is to make it as simple as + possible for the course authors. It will be designed in a future iteration. + +.. _Badges: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/enable_badging.html + +.. This section describes the forces at play, including technological, political, social, and project local. + These forces are probably in tension, and should be called out as such. The language in this section is + value-neutral. It is simply describing facts. Decision ******** -We will create a repository... - -TODO: Clearly state how the context above led to the creation of this repo. +#. We will implement the certificates mechanism as a Django app plugin. +#. This plugin will be installed on the same server as the Open edX instance. It will use the same database as the + Open edX instance to optimize the performance. This decision is made to minimize the latency that would be + introduced by querying web APIs. +#. The goal is to rely only on the public Python APIs and avoid direct access to the database or models. This is to + minimize breaking changes in the next Open edX releases. +#. The details of this plugin's architecture are described in the :doc:`0002-architecture` document. .. This section describes our response to these forces. It is stated in full sentences, with active voice. "We will …" Consequences ************ -TODO: Add what other things will change as a result of creating this repo. +We will stop using the built-in certificates mechanism. The goal of this repository is to have as little dependencies +from the core ``edx-platform`` as possible to make it easier to maintain and upgrade. -.. This section describes the resulting context, after applying the decision. All consequences should be listed here, not just the "positive" ones. A particular decision may have positive, negative, and neutral consequences, but all of them affect the team and project in the future. +.. This section describes the resulting context, after applying the decision. All consequences should be listed here, + not just the "positive" ones. A particular decision may have positive, negative, and neutral consequences, but all of + them affect the team and project in the future. Rejected Alternatives ********************* -TODO: If applicable, list viable alternatives to creating this new repo and give reasons for why they were rejected. If not applicable, remove section. +We considered: + +#. Using the certificates built into Open edX. + However, we need to rework them significantly to meet the described requirements. Given the plans to migrate + the course certificates to the `credentials`_ service, maintaining this approach could take a lot of work. +#. Using the `credentials`_ service. + Currently, this service supports only Programs. It means that it is tightly coupled to the `course-discovery`_ IDA. + We will use :ref:`Pathway ` instead of Programs to remove this dependency and gain more flexibility. + Therefore, the `credentials`_ service would also require significant reworks to support the necessary features. +#. Using the existing implementation of the `Badges`_. + This code was not maintained for a long time and was recently removed from Open edX. + +.. _credentials: https://github.com/openedx/credentials +.. _course-discovery: https://github.com/openedx/course-discovery .. This section lists alternate options considered, described briefly, with pros and cons. -References -********** +Definitions +*********** + +#. **Course** + + .. _course: + + A standard Open edX course. Also referred to as a Course XBlock or a ``CourseBlock``. + +#. **Lesson** + + .. _lesson: + + A course that consists of a single section. The `section-to-course`_ extension extracts this section from a "full" + course. + +#. **Pathway** -TODO: If applicable, add any references. If not applicable, remove section. + .. _pathway: -.. (Optional) List any additional references here that would be useful to the future reader. See `Documenting Architecture Decisions`_ and `OEP-19 on ADRs`_ for further input. + A similar concept to Open edX Programs but handled by an Open edX plugin. Once we complete the Pathway planning + phase, we will update this definition and add a link. -.. _Documenting Architecture Decisions: https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions -.. _OEP-19 on ADRs: https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0019-bp-developer-documentation.html#adrs +.. _section-to-course: https://github.com/open-craft/section-to-course/ diff --git a/docs/decisions/0002-architecture.rst b/docs/decisions/0002-architecture.rst new file mode 100644 index 0000000..2c7b885 --- /dev/null +++ b/docs/decisions/0002-architecture.rst @@ -0,0 +1,119 @@ +0002 Architecture +################# + +.. TODO: This document will be moved to a plugin repo once we have a plugin architecture. + +Status +****** + +**Provisional** + + +Context +******* + +#. This Django app generates and shows user certificates. +#. The models should store certificate configurations. The certificate types will vary between different course types. + The available course types are: + + #. :ref:`Course `. + #. :ref:`Lesson `. + #. :ref:`Pathway `. + +#. This Django app uses `celerybeat`_ to periodically retrieve data from the external API. Retrieving this data is + pluggable - it means that other developers can develop a Python package and install it to have a custom ways to + retrieve data from different APIs. +#. If a user matches the criteria, the certificates will be generated from a PDF template (stored in the assets model). + The PDF will be uploaded to S3, and the link will be sent to the user. The generation process should also be pluggable - it means + that other developers can develop a Python package and install it to have custom ways to generate certificates. + +.. _celerybeat: https://django-celery-beat.readthedocs.io/en/latest/ + + +Decision +******** + +.. graphviz:: + + digraph G { + node [shape=box, style=filled, fillcolor=gray95] + edge [fontcolor=black, color=black] + + subgraph cluster_0 { + label = "Open edX"; + style=filled; + color=lightgrey; + + // Resources + LMS; + } + + subgraph cluster_1 { + label = "openedx-certificates"; + style=filled; + color=lightgrey; + + // Resources/models + CertificateType [label="ExternalCertificateType"] + CourseConfiguration [label="ExternalCertificateCourseConfiguration"] + Certificate [label="ExternalCertificate"] + Asset [label="ExternalCertificateAsset"] + PeriodicTask + Schedule + + // Processes + Processing [shape=ellipse] + Generation [shape=ellipse] + + // DB relations + edge [fontcolor=black, color=gray50] + CertificateType -> CourseConfiguration [dir=back, headlabel="0..*", taillabel="1 "] + CourseConfiguration -> PeriodicTask [dir=both, headlabel="1 ", taillabel="1"] + + // Non-DB relations + edge [fontcolor=black, color=blue] + CourseConfiguration -> Generation + Asset -> Generation + PeriodicTask -> Schedule + + // Processes + edge [fontcolor=black, color=red] + Schedule -> Processing [label="trigger"] + Processing -> Generation [label="provide elgible users"] + Generation -> Certificate [label="generate certificates"] + } + + + // Processes involving external APIs. + edge [fontcolor=black, color=red] + Processing -> LMS [label="pull data", dir=forward] + + } + + +User stories +************ + +TODO: Move this to the docs. + +As an Instructor, I want to enable certificate generation for a course. +======================================================================= + +To do this, I should: + +#. Visit course certificate admin page. +#. Create a new entry with a course ID, certificate type and an "Enabled" toggle. +#. Internally, each of these entries will be a cron task. This way, we can set individual certificate generation schedules. + It means that an Instructor can schedule generating different certificates for the same course at different times. + +Once done, the celery cron will be scheduled to run at the specified time. The celery task will: + +#. Retrieve data from the external API. +#. Check which users are eligible for a certificate. +#. Generate certificates for the eligible users. + + +Questions: + +#. Should we use course's start/end date to gate cert generation? +#. Maybe we could disable the cron task when the course is closed? diff --git a/docs/getting_started.rst b/docs/getting_started.rst index cec5418..929795d 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -1,18 +1,68 @@ Getting Started ############### +Developing +********** + If you have not already done so, create/activate a `virtualenv`_. Unless otherwise stated, assume all terminal code below is executed within the virtualenv. .. _virtualenv: https://virtualenvwrapper.readthedocs.org/en/latest/ +One Time Setup +============== +.. code-block:: bash + + # Clone the repository + git clone git@github.com:open-craft/openedx-certificates.git + cd openedx-certificates + + # Set up a virtualenv with the same name as the repo and activate it + # Here's how you might do that if you have virtualenvwrapper setup. + mkvirtualenv -p python3.8 openedx-certificates -Install dependencies -******************** -Dependencies can be installed via the command below. + # Install project dependencies + make requirements + +Every time you develop something in this repo +============================================= .. code-block:: bash - $ make requirements + # Activate the virtualenv + # Here's how you might do that if you're using virtualenvwrapper. + workon openedx-certificates + + # Grab the latest code + git checkout main + git pull + + # Install/update the dev requirements + make requirements + + # Run the tests and quality checks (to verify the status before you make any changes) + make validate + + # Make a new branch for your changes + git checkout -b / + + # Using your favorite editor, edit the code to make your change. + vim ... + + # Run your new tests + pytest ./path/to/new/tests + + # Run all the tests and quality checks + make validate + + # Commit all your changes + git commit ... + git push + + # Open a PR and ask for review. + +Deploying +********* +TODO: Document this. From f17423bfd219dadf4b32c8676a69a16167a5262e Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 24 Oct 2023 23:57:12 +0200 Subject: [PATCH 04/46] docs: add repo visualization to README --- README.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1a98024..a91f4f2 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ openedx-certificates #################### |pypi-badge| |ci-badge| |codecov-badge| |doc-badge| |pyversions-badge| -|license-badge| |status-badge| +|license-badge| |status-badge| |visualization-badge| Purpose ******* @@ -107,3 +107,8 @@ Please do not report security issues in public. Please email security@openedx.or .. |status-badge| image:: https://img.shields.io/badge/Status-Experimental-yellow :alt: Status + +.. https://githubnext.com/projects/repo-visualization/ +.. |visualization-badge| image:: https://img.shields.io/badge/Repo%20Visualization-8A2BE2 + :target: https://mango-dune-07a8b7110.1.azurestaticapps.net/?repo=open-craft/openedx-certificates + :alt: Visualization From 8045f3baf677fd06bceb77865415630c5df0e44b Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 24 Oct 2023 23:57:31 +0200 Subject: [PATCH 05/46] docs: update CHANGELOG --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2d559ca..a2d4874 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,10 +16,10 @@ Unreleased * -0.1.0 – 2023-08-17 +0.1.0 – 2023-09-17 ********************************************** Added ===== -* First release on PyPI. +* Initial implementation of the certificates app. From 2189a53d51d269c814f8ce00df9ac6a539f29eca Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 24 Oct 2023 23:58:08 +0200 Subject: [PATCH 06/46] chore: update requirements --- requirements/base.in | 14 +- requirements/base.txt | 252 +++++++++++++++++++++++- requirements/ci.txt | 24 ++- requirements/constraints.txt | 3 + requirements/dev.txt | 367 ++++++++++++++++++++++++++++++++-- requirements/doc.txt | 370 +++++++++++++++++++++++++++++++---- requirements/pip-tools.txt | 10 +- requirements/pip.txt | 6 +- requirements/quality.txt | 353 +++++++++++++++++++++++++++++++-- requirements/test.txt | 370 +++++++++++++++++++++++++++++++++-- 10 files changed, 1660 insertions(+), 109 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index 15d3577..f747a23 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,5 +1,15 @@ # Core requirements for using this application -c constraints.txt -Django # Web application framework -django-model-utils # Provides TimeStampedModel abstract base class +django # Web application framework +django-model-utils # Provides TimeStampedModel abstract base class +edx-opaque-keys # Create and introspect Course and XBlock identities +celery # Distributed task queue +django-celery-beat # Periodic task scheduler +django_reverse_admin # Provides reverse inlines in the admin interface +djangorestframework # RESTful API framework +# TODO: Extract these to a plugin. +pypdf # PDF manipulation library +reportlab # PDF generation library +openedx-completion-aggregator # Completion aggregation service +edx_ace # Messaging library diff --git a/requirements/base.txt b/requirements/base.txt index 7282fc4..7566169 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,18 +4,260 @@ # # make upgrade # +amqp==5.2.0 + # via kombu +appdirs==1.4.4 + # via fs asgiref==3.7.2 # via django -django==3.2.20 +attrs==23.1.0 + # via edx-ace +backports-zoneinfo[tzdata]==0.2.1 + # via + # celery + # django-celery-beat + # django-timezone-field + # kombu +billiard==4.2.0 + # via celery +celery==5.3.5 + # via + # -r requirements/base.in + # django-celery-beat + # edx-celeryutils + # event-tracking + # openedx-completion-aggregator +certifi==2023.7.22 + # via requests +cffi==1.16.0 + # via + # cryptography + # pynacl +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # celery + # click-didyoumean + # click-plugins + # click-repl + # code-annotations + # edx-django-utils +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.3.0 + # via celery +code-annotations==1.5.0 + # via edx-toggles +cron-descriptor==1.4.0 + # via django-celery-beat +cryptography==41.0.5 + # via pyjwt +django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # django-celery-beat + # django-crum # django-model-utils + # django-timezone-field + # django-waffle + # djangorestframework + # drf-jwt + # edx-ace + # edx-celeryutils + # edx-completion + # edx-django-utils + # edx-drf-extensions + # edx-toggles + # event-tracking + # jsonfield + # openedx-completion-aggregator +django-celery-beat==2.5.0 + # via -r requirements/base.in +django-crum==0.7.9 + # via + # edx-django-utils + # edx-toggles django-model-utils==4.3.1 + # via + # -r requirements/base.in + # edx-celeryutils + # edx-completion + # openedx-completion-aggregator +django-object-actions==4.2.0 # via -r requirements/base.in -pytz==2023.3 - # via django +django-reverse-admin==2.9.6 + # via -r requirements/base.in +django-timezone-field==6.0.1 + # via django-celery-beat +django-waffle==4.0.0 + # via + # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/base.in + # drf-jwt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +drf-jwt==1.19.2 + # via edx-drf-extensions +edx-ace==1.7.0 + # via -r requirements/base.in +edx-celeryutils==1.2.3 + # via openedx-completion-aggregator +edx-completion==4.4.0 + # via openedx-completion-aggregator +edx-django-utils==5.8.0 + # via + # edx-drf-extensions + # edx-toggles + # event-tracking +edx-drf-extensions==8.13.0 + # via edx-completion +edx-opaque-keys==2.5.1 + # via + # -r requirements/base.in + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +edx-toggles==5.1.0 + # via + # edx-completion + # openedx-completion-aggregator +event-tracking==2.2.0 + # via edx-completion +fs==2.4.16 + # via xblock +idna==3.4 + # via requests +jinja2==3.1.2 + # via code-annotations +jsonfield==3.1.0 + # via edx-celeryutils +kombu==5.3.3 + # via celery +lxml==4.9.3 + # via xblock +mako==1.3.0 + # via xblock +markupsafe==2.1.3 + # via + # jinja2 + # mako + # xblock +newrelic==9.1.2 + # via edx-django-utils +openedx-completion-aggregator==4.0.3 + # via -r requirements/base.in +pbr==6.0.0 + # via stevedore +pillow==10.1.0 + # via reportlab +prompt-toolkit==3.0.40 + # via click-repl +psutil==5.9.6 + # via edx-django-utils +pycparser==2.21 + # via cffi +pyjwt[crypto]==2.8.0 + # via + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via + # edx-opaque-keys + # event-tracking +pynacl==1.5.0 + # via edx-django-utils +pypdf==3.17.0 + # via -r requirements/base.in +python-crontab==3.0.0 + # via django-celery-beat +python-dateutil==2.8.2 + # via + # celery + # edx-ace + # python-crontab + # xblock +python-slugify==8.0.1 + # via code-annotations +pytz==2023.3.post1 + # via + # django + # djangorestframework + # edx-completion + # event-tracking + # xblock +pyyaml==6.0.1 + # via + # code-annotations + # xblock +reportlab==4.0.7 + # via -r requirements/base.in +requests==2.31.0 + # via + # edx-drf-extensions + # sailthru-client +sailthru-client==2.2.3 + # via edx-ace +semantic-version==2.10.0 + # via edx-drf-extensions +simplejson==3.19.2 + # via + # sailthru-client + # xblock +six==1.16.0 + # via + # edx-ace + # event-tracking + # fs + # openedx-completion-aggregator + # python-dateutil sqlparse==0.4.4 # via django -typing-extensions==4.7.1 - # via asgiref +stevedore==5.1.0 + # via + # code-annotations + # edx-ace + # edx-django-utils + # edx-opaque-keys +text-unidecode==1.3 + # via python-slugify +typing-extensions==4.8.0 + # via + # asgiref + # edx-opaque-keys + # kombu + # pypdf +tzdata==2023.3 + # via + # backports-zoneinfo + # celery + # django-celery-beat +urllib3==2.1.0 + # via requests +vine==5.1.0 + # via + # amqp + # celery + # kombu +wcwidth==0.2.10 + # via prompt-toolkit +web-fragments==2.1.0 + # via xblock +webob==1.8.7 + # via xblock +xblock==1.8.1 + # via + # edx-completion + # openedx-completion-aggregator + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/ci.txt b/requirements/ci.txt index 6b50e71..c2a3c5e 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -6,28 +6,34 @@ # distlib==0.3.7 # via virtualenv -filelock==3.12.2 +filelock==3.13.1 # via # tox # virtualenv -packaging==23.1 - # via tox -platformdirs==3.10.0 - # via virtualenv -pluggy==1.2.0 +packaging==23.2 + # via + # pyproject-api + # tox +platformdirs==3.11.0 + # via + # -c requirements/constraints.txt + # tox + # virtualenv +pluggy==1.3.0 # via tox py==1.11.0 # via tox six==1.16.0 # via tox tomli==2.0.1 - # via tox + # via + # pyproject-api + # tox tox==3.28.0 # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/ci.in # tox-battery tox-battery==0.6.2 # via -r requirements/ci.in -virtualenv==20.24.3 +virtualenv==20.24.6 # via tox diff --git a/requirements/constraints.txt b/requirements/constraints.txt index d91704b..6786d1d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -10,3 +10,6 @@ # Common constraints for edx repos -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + +# Currently, `virtualenv` and `black` use different versions of `platformdirs`. +platformdirs<4.0.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 7b9b149..17dc3a2 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,59 +4,238 @@ # # make upgrade # +amqp==5.2.0 + # via + # -r requirements/quality.txt + # kombu +appdirs==1.4.4 + # via + # -r requirements/quality.txt + # fs asgiref==3.7.2 # via # -r requirements/quality.txt # django -black==23.7.0 +attrs==23.1.0 + # via + # -r requirements/quality.txt + # edx-ace +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/quality.txt + # backports-zoneinfo + # celery + # django-celery-beat + # django-timezone-field + # kombu +billiard==4.2.0 + # via + # -r requirements/quality.txt + # celery +black==23.11.0 # via -r requirements/quality.txt -build==0.10.0 +build==1.0.3 # via # -r requirements/pip-tools.txt # pip-tools +celery==5.3.5 + # via + # -r requirements/quality.txt + # django-celery-beat + # edx-celeryutils + # event-tracking + # openedx-completion-aggregator +certifi==2023.7.22 + # via + # -r requirements/quality.txt + # requests +cffi==1.16.0 + # via + # -r requirements/quality.txt + # cryptography + # pynacl chardet==5.2.0 # via diff-cover +charset-normalizer==3.3.2 + # via + # -r requirements/quality.txt + # requests click==8.1.7 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # black + # celery + # click-didyoumean + # click-plugins + # click-repl # code-annotations + # edx-django-utils # pip-tools +click-didyoumean==0.3.0 + # via + # -r requirements/quality.txt + # celery +click-plugins==1.1.1 + # via + # -r requirements/quality.txt + # celery +click-repl==0.3.0 + # via + # -r requirements/quality.txt + # celery code-annotations==1.5.0 - # via -r requirements/quality.txt -coverage[toml]==7.3.0 # via # -r requirements/quality.txt + # edx-toggles +coverage[toml]==7.3.2 + # via + # -r requirements/quality.txt + # coverage # django-coverage-plugin # pytest-cov -diff-cover==7.7.0 +cron-descriptor==1.4.0 + # via + # -r requirements/quality.txt + # django-celery-beat +cryptography==41.0.5 + # via + # -r requirements/quality.txt + # pyjwt +diff-cover==8.0.1 # via -r requirements/dev.in distlib==0.3.7 # via # -r requirements/ci.txt # virtualenv -django==3.2.20 +dj-inmemorystorage==2.1.0 + # via -r requirements/quality.txt +django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt + # dj-inmemorystorage + # django-celery-beat + # django-crum # django-model-utils + # django-timezone-field + # django-waffle + # djangorestframework + # drf-jwt + # edx-ace + # edx-celeryutils + # edx-completion + # edx-django-utils + # edx-drf-extensions # edx-i18n-tools + # edx-toggles + # event-tracking + # jsonfield + # openedx-completion-aggregator +django-celery-beat==2.5.0 + # via -r requirements/quality.txt django-coverage-plugin==3.1.0 # via -r requirements/quality.txt +django-crum==0.7.9 + # via + # -r requirements/quality.txt + # edx-django-utils + # edx-toggles django-model-utils==4.3.1 + # via + # -r requirements/quality.txt + # edx-celeryutils + # edx-completion + # openedx-completion-aggregator +django-object-actions==4.2.0 + # via -r requirements/quality.txt +django-reverse-admin==2.9.6 + # via -r requirements/quality.txt +django-timezone-field==6.0.1 + # via + # -r requirements/quality.txt + # django-celery-beat +django-waffle==4.0.0 + # via + # -r requirements/quality.txt + # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/quality.txt + # drf-jwt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +drf-jwt==1.19.2 + # via + # -r requirements/quality.txt + # edx-drf-extensions +edx-ace==1.7.0 # via -r requirements/quality.txt -edx-i18n-tools==1.1.0 +edx-celeryutils==1.2.3 + # via + # -r requirements/quality.txt + # openedx-completion-aggregator +edx-completion==4.4.0 + # via + # -r requirements/quality.txt + # openedx-completion-aggregator +edx-django-utils==5.8.0 + # via + # -r requirements/quality.txt + # edx-drf-extensions + # edx-toggles + # event-tracking +edx-drf-extensions==8.13.0 + # via + # -r requirements/quality.txt + # edx-completion +edx-i18n-tools==1.3.0 # via -r requirements/dev.in +edx-opaque-keys==2.5.1 + # via + # -r requirements/quality.txt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +edx-toggles==5.1.0 + # via + # -r requirements/quality.txt + # edx-completion + # openedx-completion-aggregator +event-tracking==2.2.0 + # via + # -r requirements/quality.txt + # edx-completion exceptiongroup==1.1.3 # via # -r requirements/quality.txt # pytest -filelock==3.12.2 +factory-boy==3.3.0 + # via -r requirements/quality.txt +faker==20.0.0 + # via + # -r requirements/quality.txt + # factory-boy +filelock==3.13.1 # via # -r requirements/ci.txt # tox # virtualenv +fs==2.4.16 + # via + # -r requirements/quality.txt + # xblock +idna==3.4 + # via + # -r requirements/quality.txt + # requests +importlib-metadata==6.8.0 + # via + # -r requirements/pip-tools.txt + # build iniconfig==2.0.0 # via # -r requirements/quality.txt @@ -66,15 +245,40 @@ jinja2==3.1.2 # -r requirements/quality.txt # code-annotations # diff-cover +jsonfield==3.1.0 + # via + # -r requirements/quality.txt + # edx-celeryutils +kombu==5.3.3 + # via + # -r requirements/quality.txt + # celery +lxml==4.9.3 + # via + # -r requirements/quality.txt + # edx-i18n-tools + # xblock +mako==1.3.0 + # via + # -r requirements/quality.txt + # xblock markupsafe==2.1.3 # via # -r requirements/quality.txt # jinja2 + # mako + # xblock mypy-extensions==1.0.0 # via # -r requirements/quality.txt # black -packaging==23.1 +newrelic==9.1.2 + # via + # -r requirements/quality.txt + # edx-django-utils +openedx-completion-aggregator==4.0.3 + # via -r requirements/quality.txt +packaging==23.2 # via # -r requirements/ci.txt # -r requirements/pip-tools.txt @@ -90,19 +294,24 @@ pathspec==0.11.2 # -r requirements/quality.txt # black # yamllint -pbr==5.11.1 +pbr==6.0.0 # via # -r requirements/quality.txt # stevedore +pillow==10.1.0 + # via + # -r requirements/quality.txt + # reportlab pip-tools==7.3.0 # via -r requirements/pip-tools.txt -platformdirs==3.10.0 +platformdirs==3.11.0 # via + # -c requirements/constraints.txt # -r requirements/ci.txt # -r requirements/quality.txt # black # virtualenv -pluggy==1.2.0 +pluggy==1.3.0 # via # -r requirements/ci.txt # -r requirements/quality.txt @@ -111,44 +320,117 @@ pluggy==1.2.0 # tox polib==1.2.0 # via edx-i18n-tools +prompt-toolkit==3.0.40 + # via + # -r requirements/quality.txt + # click-repl +psutil==5.9.6 + # via + # -r requirements/quality.txt + # edx-django-utils py==1.11.0 # via # -r requirements/ci.txt # tox +pycparser==2.21 + # via + # -r requirements/quality.txt + # cffi pygments==2.16.1 # via diff-cover +pyjwt[crypto]==2.8.0 + # via + # -r requirements/quality.txt + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via + # -r requirements/quality.txt + # edx-opaque-keys + # event-tracking +pynacl==1.5.0 + # via + # -r requirements/quality.txt + # edx-django-utils +pypdf==3.17.0 + # via -r requirements/quality.txt pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt # build -pytest==7.4.0 +pytest==7.4.3 # via # -r requirements/quality.txt # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/quality.txt -pytest-django==4.5.2 +pytest-django==4.7.0 # via -r requirements/quality.txt +python-crontab==3.0.0 + # via + # -r requirements/quality.txt + # django-celery-beat +python-dateutil==2.8.2 + # via + # -r requirements/quality.txt + # celery + # edx-ace + # faker + # python-crontab + # xblock python-slugify==8.0.1 # via # -r requirements/quality.txt # code-annotations -pytz==2023.3 +pytz==2023.3.post1 # via # -r requirements/quality.txt # django + # djangorestframework + # edx-completion + # event-tracking + # xblock pyyaml==6.0.1 # via # -r requirements/quality.txt # code-annotations # edx-i18n-tools + # xblock # yamllint -ruff==0.0.286 +reportlab==4.0.7 + # via -r requirements/quality.txt +requests==2.31.0 + # via + # -r requirements/quality.txt + # edx-drf-extensions + # sailthru-client +ruff==0.1.5 # via -r requirements/quality.txt +sailthru-client==2.2.3 + # via + # -r requirements/quality.txt + # edx-ace +semantic-version==2.10.0 + # via + # -r requirements/quality.txt + # edx-drf-extensions +simplejson==3.19.2 + # via + # -r requirements/quality.txt + # sailthru-client + # xblock six==1.16.0 # via # -r requirements/ci.txt + # -r requirements/quality.txt + # dj-inmemorystorage + # edx-ace + # event-tracking + # fs + # openedx-completion-aggregator + # python-dateutil # tox sqlparse==0.4.4 # via @@ -158,6 +440,9 @@ stevedore==5.1.0 # via # -r requirements/quality.txt # code-annotations + # edx-ace + # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/quality.txt @@ -176,26 +461,66 @@ tomli==2.0.1 # tox tox==3.28.0 # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/ci.txt # tox-battery tox-battery==0.6.2 # via -r requirements/ci.txt -typing-extensions==4.7.1 +typing-extensions==4.8.0 # via # -r requirements/quality.txt # asgiref # black -virtualenv==20.24.3 + # edx-opaque-keys + # faker + # kombu + # pypdf +tzdata==2023.3 + # via + # -r requirements/quality.txt + # backports-zoneinfo + # celery + # django-celery-beat +urllib3==2.1.0 + # via + # -r requirements/quality.txt + # requests +vine==5.1.0 + # via + # -r requirements/quality.txt + # amqp + # celery + # kombu +virtualenv==20.24.6 # via # -r requirements/ci.txt # tox -wheel==0.41.2 +wcwidth==0.2.10 + # via + # -r requirements/quality.txt + # prompt-toolkit +web-fragments==2.1.0 + # via + # -r requirements/quality.txt + # xblock +webob==1.8.7 + # via + # -r requirements/quality.txt + # xblock +wheel==0.41.3 # via # -r requirements/pip-tools.txt # pip-tools -yamllint==1.32.0 +xblock==1.8.1 + # via + # -r requirements/quality.txt + # edx-completion + # openedx-completion-aggregator +yamllint==1.33.0 # via -r requirements/quality.txt +zipp==3.17.0 + # via + # -r requirements/pip-tools.txt + # importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/doc.txt b/requirements/doc.txt index ff373de..5e758c7 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -8,48 +8,161 @@ accessible-pygments==0.0.4 # via pydata-sphinx-theme alabaster==0.7.13 # via sphinx +amqp==5.2.0 + # via + # -r requirements/test.txt + # kombu +appdirs==1.4.4 + # via + # -r requirements/test.txt + # fs asgiref==3.7.2 # via # -r requirements/test.txt # django -babel==2.12.1 +attrs==23.1.0 + # via + # -r requirements/test.txt + # edx-ace +babel==2.13.1 # via # pydata-sphinx-theme # sphinx +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/test.txt + # backports-zoneinfo + # celery + # django-celery-beat + # django-timezone-field + # kombu beautifulsoup4==4.12.2 # via pydata-sphinx-theme -bleach==6.0.0 - # via readme-renderer -build==0.10.0 +billiard==4.2.0 + # via + # -r requirements/test.txt + # celery +build==1.0.3 # via -r requirements/doc.in +celery==5.3.5 + # via + # -r requirements/test.txt + # django-celery-beat + # edx-celeryutils + # event-tracking + # openedx-completion-aggregator certifi==2023.7.22 - # via requests -cffi==1.15.1 - # via cryptography -charset-normalizer==3.2.0 - # via requests + # via + # -r requirements/test.txt + # requests +cffi==1.16.0 + # via + # -r requirements/test.txt + # cryptography + # pynacl +charset-normalizer==3.3.2 + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt + # celery + # click-didyoumean + # click-plugins + # click-repl # code-annotations + # edx-django-utils +click-didyoumean==0.3.0 + # via + # -r requirements/test.txt + # celery +click-plugins==1.1.1 + # via + # -r requirements/test.txt + # celery +click-repl==0.3.0 + # via + # -r requirements/test.txt + # celery code-annotations==1.5.0 - # via -r requirements/test.txt -coverage[toml]==7.3.0 # via # -r requirements/test.txt + # edx-toggles +coverage[toml]==7.3.2 + # via + # -r requirements/test.txt + # coverage # django-coverage-plugin # pytest-cov -cryptography==41.0.3 - # via secretstorage -django==3.2.20 +cron-descriptor==1.4.0 + # via + # -r requirements/test.txt + # django-celery-beat +cryptography==41.0.5 + # via + # -r requirements/test.txt + # pyjwt + # secretstorage +dj-inmemorystorage==2.1.0 + # via -r requirements/test.txt +django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # dj-inmemorystorage + # django-celery-beat + # django-crum # django-model-utils + # django-timezone-field + # django-waffle + # djangorestframework + # drf-jwt + # edx-ace + # edx-celeryutils + # edx-completion + # edx-django-utils + # edx-drf-extensions + # edx-toggles + # event-tracking + # jsonfield + # openedx-completion-aggregator +django-celery-beat==2.5.0 + # via -r requirements/test.txt django-coverage-plugin==3.1.0 # via -r requirements/test.txt +django-crum==0.7.9 + # via + # -r requirements/test.txt + # edx-django-utils + # edx-toggles django-model-utils==4.3.1 + # via + # -r requirements/test.txt + # edx-celeryutils + # edx-completion + # openedx-completion-aggregator +django-object-actions==4.2.0 + # via -r requirements/test.txt +django-reverse-admin==2.9.6 # via -r requirements/test.txt +django-timezone-field==6.0.1 + # via + # -r requirements/test.txt + # django-celery-beat +django-waffle==4.0.0 + # via + # -r requirements/test.txt + # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator doc8==1.1.1 # via -r requirements/doc.in docutils==0.19 @@ -59,20 +172,72 @@ docutils==0.19 # readme-renderer # restructuredtext-lint # sphinx +drf-jwt==1.19.2 + # via + # -r requirements/test.txt + # edx-drf-extensions +edx-ace==1.7.0 + # via -r requirements/test.txt +edx-celeryutils==1.2.3 + # via + # -r requirements/test.txt + # openedx-completion-aggregator +edx-completion==4.4.0 + # via + # -r requirements/test.txt + # openedx-completion-aggregator +edx-django-utils==5.8.0 + # via + # -r requirements/test.txt + # edx-drf-extensions + # edx-toggles + # event-tracking +edx-drf-extensions==8.13.0 + # via + # -r requirements/test.txt + # edx-completion +edx-opaque-keys==2.5.1 + # via + # -r requirements/test.txt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +edx-toggles==5.1.0 + # via + # -r requirements/test.txt + # edx-completion + # openedx-completion-aggregator +event-tracking==2.2.0 + # via + # -r requirements/test.txt + # edx-completion exceptiongroup==1.1.3 # via # -r requirements/test.txt # pytest +factory-boy==3.3.0 + # via -r requirements/test.txt +faker==20.0.0 + # via + # -r requirements/test.txt + # factory-boy +fs==2.4.16 + # via + # -r requirements/test.txt + # xblock idna==3.4 - # via requests + # via + # -r requirements/test.txt + # requests imagesize==1.4.1 # via sphinx importlib-metadata==6.8.0 # via + # build # keyring # sphinx # twine -importlib-resources==6.0.1 +importlib-resources==6.1.1 # via keyring iniconfig==2.0.0 # via @@ -89,38 +254,78 @@ jinja2==3.1.2 # -r requirements/test.txt # code-annotations # sphinx -keyring==24.2.0 +jsonfield==3.1.0 + # via + # -r requirements/test.txt + # edx-celeryutils +keyring==24.3.0 # via twine +kombu==5.3.3 + # via + # -r requirements/test.txt + # celery +lxml==4.9.3 + # via + # -r requirements/test.txt + # xblock +mako==1.3.0 + # via + # -r requirements/test.txt + # xblock markdown-it-py==3.0.0 # via rich markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 + # mako + # xblock mdurl==0.1.2 # via markdown-it-py more-itertools==10.1.0 # via jaraco-classes -packaging==23.1 +newrelic==9.1.2 + # via + # -r requirements/test.txt + # edx-django-utils +nh3==0.2.14 + # via readme-renderer +openedx-completion-aggregator==4.0.3 + # via -r requirements/test.txt +packaging==23.2 # via # -r requirements/test.txt # build # pydata-sphinx-theme # pytest # sphinx -pbr==5.11.1 +pbr==6.0.0 # via # -r requirements/test.txt # stevedore +pillow==10.1.0 + # via + # -r requirements/test.txt + # reportlab pkginfo==1.9.6 # via twine -pluggy==1.2.0 +pluggy==1.3.0 # via # -r requirements/test.txt # pytest +prompt-toolkit==3.0.40 + # via + # -r requirements/test.txt + # click-repl +psutil==5.9.6 + # via + # -r requirements/test.txt + # edx-django-utils pycparser==2.21 - # via cffi -pydata-sphinx-theme==0.13.3 + # via + # -r requirements/test.txt + # cffi +pydata-sphinx-theme==0.14.3 # via sphinx-book-theme pygments==2.16.1 # via @@ -130,35 +335,74 @@ pygments==2.16.1 # readme-renderer # rich # sphinx +pyjwt[crypto]==2.8.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via + # -r requirements/test.txt + # edx-opaque-keys + # event-tracking +pynacl==1.5.0 + # via + # -r requirements/test.txt + # edx-django-utils +pypdf==3.17.0 + # via -r requirements/test.txt pyproject-hooks==1.0.0 # via build -pytest==7.4.0 +pytest==7.4.3 # via # -r requirements/test.txt # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/test.txt -pytest-django==4.5.2 +pytest-django==4.7.0 # via -r requirements/test.txt +python-crontab==3.0.0 + # via + # -r requirements/test.txt + # django-celery-beat +python-dateutil==2.8.2 + # via + # -r requirements/test.txt + # celery + # edx-ace + # faker + # python-crontab + # xblock python-slugify==8.0.1 # via # -r requirements/test.txt # code-annotations -pytz==2023.3 +pytz==2023.3.post1 # via # -r requirements/test.txt # babel # django + # djangorestframework + # edx-completion + # event-tracking + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations -readme-renderer==41.0 + # xblock +readme-renderer==42.0 # via twine +reportlab==4.0.7 + # via -r requirements/test.txt requests==2.31.0 # via + # -r requirements/test.txt + # edx-drf-extensions # requests-toolbelt + # sailthru-client # sphinx # twine requests-toolbelt==1.0.0 @@ -167,15 +411,35 @@ restructuredtext-lint==1.4.0 # via doc8 rfc3986==2.0.0 # via twine -rich==13.5.2 +rich==13.6.0 # via twine +sailthru-client==2.2.3 + # via + # -r requirements/test.txt + # edx-ace secretstorage==3.3.3 # via keyring +semantic-version==2.10.0 + # via + # -r requirements/test.txt + # edx-drf-extensions +simplejson==3.19.2 + # via + # -r requirements/test.txt + # sailthru-client + # xblock six==1.16.0 - # via bleach + # via + # -r requirements/test.txt + # dj-inmemorystorage + # edx-ace + # event-tracking + # fs + # openedx-completion-aggregator + # python-dateutil snowballstemmer==2.2.0 # via sphinx -soupsieve==2.4.1 +soupsieve==2.5 # via beautifulsoup4 sphinx==6.2.1 # via @@ -205,6 +469,9 @@ stevedore==5.1.0 # -r requirements/test.txt # code-annotations # doc8 + # edx-ace + # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/test.txt @@ -219,19 +486,54 @@ tomli==2.0.1 # pytest twine==4.0.2 # via -r requirements/doc.in -typing-extensions==4.7.1 +typing-extensions==4.8.0 # via # -r requirements/test.txt # asgiref + # edx-opaque-keys + # faker + # kombu # pydata-sphinx-theme + # pypdf # rich -urllib3==2.0.4 +tzdata==2023.3 + # via + # -r requirements/test.txt + # backports-zoneinfo + # celery + # django-celery-beat +urllib3==2.1.0 # via + # -r requirements/test.txt # requests # twine -webencodings==0.5.1 - # via bleach -zipp==3.16.2 +vine==5.1.0 + # via + # -r requirements/test.txt + # amqp + # celery + # kombu +wcwidth==0.2.10 + # via + # -r requirements/test.txt + # prompt-toolkit +web-fragments==2.1.0 + # via + # -r requirements/test.txt + # xblock +webob==1.8.7 + # via + # -r requirements/test.txt + # xblock +xblock==1.8.1 + # via + # -r requirements/test.txt + # edx-completion + # openedx-completion-aggregator +zipp==3.17.0 # via # importlib-metadata # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 007ed38..ea34731 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -4,11 +4,13 @@ # # make upgrade # -build==0.10.0 +build==1.0.3 # via pip-tools click==8.1.7 # via pip-tools -packaging==23.1 +importlib-metadata==6.8.0 + # via build +packaging==23.2 # via build pip-tools==7.3.0 # via -r requirements/pip-tools.in @@ -19,8 +21,10 @@ tomli==2.0.1 # build # pip-tools # pyproject-hooks -wheel==0.41.2 +wheel==0.41.3 # via pip-tools +zipp==3.17.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/pip.txt b/requirements/pip.txt index 13c7e84..9014f2c 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -4,11 +4,11 @@ # # make upgrade # -wheel==0.41.2 +wheel==0.41.3 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==23.2.1 +pip==23.3.1 # via -r requirements/pip.in -setuptools==68.1.2 +setuptools==68.2.2 # via -r requirements/pip.in diff --git a/requirements/quality.txt b/requirements/quality.txt index 991f75f..3541b06 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -4,37 +4,212 @@ # # make upgrade # +amqp==5.2.0 + # via + # -r requirements/test.txt + # kombu +appdirs==1.4.4 + # via + # -r requirements/test.txt + # fs asgiref==3.7.2 # via # -r requirements/test.txt # django -black==23.7.0 +attrs==23.1.0 + # via + # -r requirements/test.txt + # edx-ace +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/test.txt + # backports-zoneinfo + # celery + # django-celery-beat + # django-timezone-field + # kombu +billiard==4.2.0 + # via + # -r requirements/test.txt + # celery +black==23.11.0 # via -r requirements/quality.in +celery==5.3.5 + # via + # -r requirements/test.txt + # django-celery-beat + # edx-celeryutils + # event-tracking + # openedx-completion-aggregator +certifi==2023.7.22 + # via + # -r requirements/test.txt + # requests +cffi==1.16.0 + # via + # -r requirements/test.txt + # cryptography + # pynacl +charset-normalizer==3.3.2 + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt # black + # celery + # click-didyoumean + # click-plugins + # click-repl # code-annotations + # edx-django-utils +click-didyoumean==0.3.0 + # via + # -r requirements/test.txt + # celery +click-plugins==1.1.1 + # via + # -r requirements/test.txt + # celery +click-repl==0.3.0 + # via + # -r requirements/test.txt + # celery code-annotations==1.5.0 - # via -r requirements/test.txt -coverage[toml]==7.3.0 # via # -r requirements/test.txt + # edx-toggles +coverage[toml]==7.3.2 + # via + # -r requirements/test.txt + # coverage # django-coverage-plugin # pytest-cov -django==3.2.20 +cron-descriptor==1.4.0 + # via + # -r requirements/test.txt + # django-celery-beat +cryptography==41.0.5 + # via + # -r requirements/test.txt + # pyjwt +dj-inmemorystorage==2.1.0 + # via -r requirements/test.txt +django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # dj-inmemorystorage + # django-celery-beat + # django-crum # django-model-utils + # django-timezone-field + # django-waffle + # djangorestframework + # drf-jwt + # edx-ace + # edx-celeryutils + # edx-completion + # edx-django-utils + # edx-drf-extensions + # edx-toggles + # event-tracking + # jsonfield + # openedx-completion-aggregator +django-celery-beat==2.5.0 + # via -r requirements/test.txt django-coverage-plugin==3.1.0 # via -r requirements/test.txt +django-crum==0.7.9 + # via + # -r requirements/test.txt + # edx-django-utils + # edx-toggles django-model-utils==4.3.1 + # via + # -r requirements/test.txt + # edx-celeryutils + # edx-completion + # openedx-completion-aggregator +django-object-actions==4.2.0 + # via -r requirements/test.txt +django-reverse-admin==2.9.6 + # via -r requirements/test.txt +django-timezone-field==6.0.1 + # via + # -r requirements/test.txt + # django-celery-beat +django-waffle==4.0.0 + # via + # -r requirements/test.txt + # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +drf-jwt==1.19.2 + # via + # -r requirements/test.txt + # edx-drf-extensions +edx-ace==1.7.0 # via -r requirements/test.txt +edx-celeryutils==1.2.3 + # via + # -r requirements/test.txt + # openedx-completion-aggregator +edx-completion==4.4.0 + # via + # -r requirements/test.txt + # openedx-completion-aggregator +edx-django-utils==5.8.0 + # via + # -r requirements/test.txt + # edx-drf-extensions + # edx-toggles + # event-tracking +edx-drf-extensions==8.13.0 + # via + # -r requirements/test.txt + # edx-completion +edx-opaque-keys==2.5.1 + # via + # -r requirements/test.txt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +edx-toggles==5.1.0 + # via + # -r requirements/test.txt + # edx-completion + # openedx-completion-aggregator +event-tracking==2.2.0 + # via + # -r requirements/test.txt + # edx-completion exceptiongroup==1.1.3 # via # -r requirements/test.txt # pytest +factory-boy==3.3.0 + # via -r requirements/test.txt +faker==20.0.0 + # via + # -r requirements/test.txt + # factory-boy +fs==2.4.16 + # via + # -r requirements/test.txt + # xblock +idna==3.4 + # via + # -r requirements/test.txt + # requests iniconfig==2.0.0 # via # -r requirements/test.txt @@ -43,13 +218,37 @@ jinja2==3.1.2 # via # -r requirements/test.txt # code-annotations +jsonfield==3.1.0 + # via + # -r requirements/test.txt + # edx-celeryutils +kombu==5.3.3 + # via + # -r requirements/test.txt + # celery +lxml==4.9.3 + # via + # -r requirements/test.txt + # xblock +mako==1.3.0 + # via + # -r requirements/test.txt + # xblock markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 + # mako + # xblock mypy-extensions==1.0.0 # via black -packaging==23.1 +newrelic==9.1.2 + # via + # -r requirements/test.txt + # edx-django-utils +openedx-completion-aggregator==4.0.3 + # via -r requirements/test.txt +packaging==23.2 # via # -r requirements/test.txt # black @@ -58,40 +257,121 @@ pathspec==0.11.2 # via # black # yamllint -pbr==5.11.1 +pbr==6.0.0 # via # -r requirements/test.txt # stevedore -platformdirs==3.10.0 - # via black -pluggy==1.2.0 +pillow==10.1.0 + # via + # -r requirements/test.txt + # reportlab +platformdirs==3.11.0 + # via + # -c requirements/constraints.txt + # black +pluggy==1.3.0 # via # -r requirements/test.txt # pytest -pytest==7.4.0 +prompt-toolkit==3.0.40 + # via + # -r requirements/test.txt + # click-repl +psutil==5.9.6 + # via + # -r requirements/test.txt + # edx-django-utils +pycparser==2.21 + # via + # -r requirements/test.txt + # cffi +pyjwt[crypto]==2.8.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via + # -r requirements/test.txt + # edx-opaque-keys + # event-tracking +pynacl==1.5.0 + # via + # -r requirements/test.txt + # edx-django-utils +pypdf==3.17.0 + # via -r requirements/test.txt +pytest==7.4.3 # via # -r requirements/test.txt # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/test.txt -pytest-django==4.5.2 +pytest-django==4.7.0 # via -r requirements/test.txt +python-crontab==3.0.0 + # via + # -r requirements/test.txt + # django-celery-beat +python-dateutil==2.8.2 + # via + # -r requirements/test.txt + # celery + # edx-ace + # faker + # python-crontab + # xblock python-slugify==8.0.1 # via # -r requirements/test.txt # code-annotations -pytz==2023.3 +pytz==2023.3.post1 # via # -r requirements/test.txt # django + # djangorestframework + # edx-completion + # event-tracking + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # xblock # yamllint -ruff==0.0.286 +reportlab==4.0.7 + # via -r requirements/test.txt +requests==2.31.0 + # via + # -r requirements/test.txt + # edx-drf-extensions + # sailthru-client +ruff==0.1.5 # via -r requirements/quality.in +sailthru-client==2.2.3 + # via + # -r requirements/test.txt + # edx-ace +semantic-version==2.10.0 + # via + # -r requirements/test.txt + # edx-drf-extensions +simplejson==3.19.2 + # via + # -r requirements/test.txt + # sailthru-client + # xblock +six==1.16.0 + # via + # -r requirements/test.txt + # dj-inmemorystorage + # edx-ace + # event-tracking + # fs + # openedx-completion-aggregator + # python-dateutil sqlparse==0.4.4 # via # -r requirements/test.txt @@ -100,6 +380,9 @@ stevedore==5.1.0 # via # -r requirements/test.txt # code-annotations + # edx-ace + # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/test.txt @@ -110,10 +393,50 @@ tomli==2.0.1 # black # coverage # pytest -typing-extensions==4.7.1 +typing-extensions==4.8.0 # via # -r requirements/test.txt # asgiref # black -yamllint==1.32.0 + # edx-opaque-keys + # faker + # kombu + # pypdf +tzdata==2023.3 + # via + # -r requirements/test.txt + # backports-zoneinfo + # celery + # django-celery-beat +urllib3==2.1.0 + # via + # -r requirements/test.txt + # requests +vine==5.1.0 + # via + # -r requirements/test.txt + # amqp + # celery + # kombu +wcwidth==0.2.10 + # via + # -r requirements/test.txt + # prompt-toolkit +web-fragments==2.1.0 + # via + # -r requirements/test.txt + # xblock +webob==1.8.7 + # via + # -r requirements/test.txt + # xblock +xblock==1.8.1 + # via + # -r requirements/test.txt + # edx-completion + # openedx-completion-aggregator +yamllint==1.33.0 # via -r requirements/quality.in + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/test.txt b/requirements/test.txt index 8dca551..72d931f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,69 +4,405 @@ # # make upgrade # +amqp==5.2.0 + # via + # -r requirements/base.txt + # kombu +appdirs==1.4.4 + # via + # -r requirements/base.txt + # fs asgiref==3.7.2 # via # -r requirements/base.txt # django +attrs==23.1.0 + # via + # -r requirements/base.txt + # edx-ace +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/base.txt + # backports-zoneinfo + # celery + # django-celery-beat + # django-timezone-field + # kombu +billiard==4.2.0 + # via + # -r requirements/base.txt + # celery +celery==5.3.5 + # via + # -r requirements/base.txt + # django-celery-beat + # edx-celeryutils + # event-tracking + # openedx-completion-aggregator +certifi==2023.7.22 + # via + # -r requirements/base.txt + # requests +cffi==1.16.0 + # via + # -r requirements/base.txt + # cryptography + # pynacl +charset-normalizer==3.3.2 + # via + # -r requirements/base.txt + # requests click==8.1.7 - # via code-annotations + # via + # -r requirements/base.txt + # celery + # click-didyoumean + # click-plugins + # click-repl + # code-annotations + # edx-django-utils +click-didyoumean==0.3.0 + # via + # -r requirements/base.txt + # celery +click-plugins==1.1.1 + # via + # -r requirements/base.txt + # celery +click-repl==0.3.0 + # via + # -r requirements/base.txt + # celery code-annotations==1.5.0 - # via -r requirements/test.in -coverage[toml]==7.3.0 # via + # -r requirements/base.txt + # -r requirements/test.in + # edx-toggles +coverage[toml]==7.3.2 + # via + # coverage # django-coverage-plugin # pytest-cov +cron-descriptor==1.4.0 + # via + # -r requirements/base.txt + # django-celery-beat +cryptography==41.0.5 + # via + # -r requirements/base.txt + # pyjwt +dj-inmemorystorage==2.1.0 + # via -r requirements/test.in # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt + # dj-inmemorystorage + # django-celery-beat + # django-crum # django-model-utils + # django-timezone-field + # django-waffle + # djangorestframework + # drf-jwt + # edx-ace + # edx-celeryutils + # edx-completion + # edx-django-utils + # edx-drf-extensions + # edx-toggles + # event-tracking + # jsonfield + # openedx-completion-aggregator +django-celery-beat==2.5.0 + # via -r requirements/base.txt django-coverage-plugin==3.1.0 # via -r requirements/test.in +django-crum==0.7.9 + # via + # -r requirements/base.txt + # edx-django-utils + # edx-toggles django-model-utils==4.3.1 + # via + # -r requirements/base.txt + # edx-celeryutils + # edx-completion + # openedx-completion-aggregator +django-object-actions==4.2.0 # via -r requirements/base.txt +django-reverse-admin==2.9.6 + # via -r requirements/base.txt +django-timezone-field==6.0.1 + # via + # -r requirements/base.txt + # django-celery-beat +django-waffle==4.0.0 + # via + # -r requirements/base.txt + # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/base.txt + # drf-jwt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +drf-jwt==1.19.2 + # via + # -r requirements/base.txt + # edx-drf-extensions +edx-ace==1.7.0 + # via -r requirements/base.txt +edx-celeryutils==1.2.3 + # via + # -r requirements/base.txt + # openedx-completion-aggregator +edx-completion==4.4.0 + # via + # -r requirements/base.txt + # openedx-completion-aggregator +edx-django-utils==5.8.0 + # via + # -r requirements/base.txt + # edx-drf-extensions + # edx-toggles + # event-tracking +edx-drf-extensions==8.13.0 + # via + # -r requirements/base.txt + # edx-completion +edx-opaque-keys==2.5.1 + # via + # -r requirements/base.txt + # edx-completion + # edx-drf-extensions + # openedx-completion-aggregator +edx-toggles==5.1.0 + # via + # -r requirements/base.txt + # edx-completion + # openedx-completion-aggregator +event-tracking==2.2.0 + # via + # -r requirements/base.txt + # edx-completion exceptiongroup==1.1.3 # via pytest +factory-boy==3.3.0 + # via -r requirements/test.in +faker==20.0.0 + # via factory-boy +fs==2.4.16 + # via + # -r requirements/base.txt + # xblock +idna==3.4 + # via + # -r requirements/base.txt + # requests iniconfig==2.0.0 # via pytest jinja2==3.1.2 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations +jsonfield==3.1.0 + # via + # -r requirements/base.txt + # edx-celeryutils +kombu==5.3.3 + # via + # -r requirements/base.txt + # celery +lxml==4.9.3 + # via + # -r requirements/base.txt + # xblock +mako==1.3.0 + # via + # -r requirements/base.txt + # xblock markupsafe==2.1.3 - # via jinja2 -packaging==23.1 + # via + # -r requirements/base.txt + # jinja2 + # mako + # xblock +newrelic==9.1.2 + # via + # -r requirements/base.txt + # edx-django-utils +openedx-completion-aggregator==4.0.3 + # via -r requirements/base.txt +packaging==23.2 # via pytest -pbr==5.11.1 - # via stevedore -pluggy==1.2.0 +pbr==6.0.0 + # via + # -r requirements/base.txt + # stevedore +pillow==10.1.0 + # via + # -r requirements/base.txt + # reportlab +pluggy==1.3.0 # via pytest -pytest==7.4.0 +prompt-toolkit==3.0.40 + # via + # -r requirements/base.txt + # click-repl +psutil==5.9.6 + # via + # -r requirements/base.txt + # edx-django-utils +pycparser==2.21 + # via + # -r requirements/base.txt + # cffi +pyjwt[crypto]==2.8.0 + # via + # -r requirements/base.txt + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via + # -r requirements/base.txt + # edx-opaque-keys + # event-tracking +pynacl==1.5.0 + # via + # -r requirements/base.txt + # edx-django-utils +pypdf==3.17.0 + # via -r requirements/base.txt +pytest==7.4.3 # via # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/test.in -pytest-django==4.5.2 +pytest-django==4.7.0 # via -r requirements/test.in +python-crontab==3.0.0 + # via + # -r requirements/base.txt + # django-celery-beat +python-dateutil==2.8.2 + # via + # -r requirements/base.txt + # celery + # edx-ace + # faker + # python-crontab + # xblock python-slugify==8.0.1 - # via code-annotations -pytz==2023.3 + # via + # -r requirements/base.txt + # code-annotations +pytz==2023.3.post1 # via # -r requirements/base.txt # django + # djangorestframework + # edx-completion + # event-tracking + # xblock pyyaml==6.0.1 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations + # xblock +reportlab==4.0.7 + # via -r requirements/base.txt +requests==2.31.0 + # via + # -r requirements/base.txt + # edx-drf-extensions + # sailthru-client +sailthru-client==2.2.3 + # via + # -r requirements/base.txt + # edx-ace +semantic-version==2.10.0 + # via + # -r requirements/base.txt + # edx-drf-extensions +simplejson==3.19.2 + # via + # -r requirements/base.txt + # sailthru-client + # xblock +six==1.16.0 + # via + # -r requirements/base.txt + # dj-inmemorystorage + # edx-ace + # event-tracking + # fs + # openedx-completion-aggregator + # python-dateutil sqlparse==0.4.4 # via # -r requirements/base.txt # django stevedore==5.1.0 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations + # edx-ace + # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 - # via python-slugify + # via + # -r requirements/base.txt + # python-slugify tomli==2.0.1 # via # coverage # pytest -typing-extensions==4.7.1 +typing-extensions==4.8.0 # via # -r requirements/base.txt # asgiref + # edx-opaque-keys + # faker + # kombu + # pypdf +tzdata==2023.3 + # via + # -r requirements/base.txt + # backports-zoneinfo + # celery + # django-celery-beat +urllib3==2.1.0 + # via + # -r requirements/base.txt + # requests +vine==5.1.0 + # via + # -r requirements/base.txt + # amqp + # celery + # kombu +wcwidth==0.2.10 + # via + # -r requirements/base.txt + # prompt-toolkit +web-fragments==2.1.0 + # via + # -r requirements/base.txt + # xblock +webob==1.8.7 + # via + # -r requirements/base.txt + # xblock +xblock==1.8.1 + # via + # -r requirements/base.txt + # edx-completion + # openedx-completion-aggregator + +# The following packages are considered to be unsafe in a requirements file: +# setuptools From d6d910548f79e11a978e45443422f0c88af4ac17 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 24 Oct 2023 23:59:37 +0200 Subject: [PATCH 07/46] test: exclude tests from coverage --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index b7c8bd2..cf51d52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,3 +8,4 @@ omit = *admin.py */static/* */templates/* + */tests/* From 6542508459824b91f8c1f20c93a79b5893ebc858 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 25 Oct 2023 00:00:40 +0200 Subject: [PATCH 08/46] build: make `manage.py` executable --- manage.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 manage.py diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 From be229dac6b31781361d7fc2f6d66a582e5467ccb Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 25 Oct 2023 00:04:37 +0200 Subject: [PATCH 09/46] feat: initial implementation --- .annotation_safe_list.yml | 20 + openedx_certificates/admin.py | 204 ++++++++++ openedx_certificates/compat.py | 78 ++++ openedx_certificates/exceptions.py | 9 + openedx_certificates/generators.py | 153 ++++++++ .../migrations/0001_initial.py | 85 +++++ openedx_certificates/migrations/__init__.py | 0 openedx_certificates/models.py | 350 +++++++++++++++++- openedx_certificates/pipelines.py | 18 + openedx_certificates/processors.py | 224 +++++++++++ openedx_certificates/tasks.py | 54 +++ openedx_certificates/views.py | 1 + requirements/base.in | 1 + setup.py | 5 + test_settings.py | 11 + tests/test_generators.py | 34 ++ tests/test_models.py | 30 +- tests/test_processors.py | 91 +++++ 18 files changed, 1349 insertions(+), 19 deletions(-) create mode 100644 openedx_certificates/admin.py create mode 100644 openedx_certificates/compat.py create mode 100644 openedx_certificates/exceptions.py create mode 100644 openedx_certificates/generators.py create mode 100644 openedx_certificates/migrations/0001_initial.py create mode 100644 openedx_certificates/migrations/__init__.py create mode 100644 openedx_certificates/pipelines.py create mode 100644 openedx_certificates/processors.py create mode 100644 openedx_certificates/tasks.py create mode 100644 openedx_certificates/views.py create mode 100644 tests/test_generators.py create mode 100644 tests/test_processors.py diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml index e4eb085..4babccc 100644 --- a/.annotation_safe_list.yml +++ b/.annotation_safe_list.yml @@ -19,6 +19,26 @@ auth.User: ".. pii_retirement": consumer_api contenttypes.ContentType: ".. no_pii:": This model has no PII +completion.BlockCompletion: + ".. no_pii:": This model has no PII +completion_aggregator.Aggregator: + ".. no_pii:": This model has no PII +completion_aggregator.CacheGroupInvalidation: + ".. no_pii:": This model has no PII +completion_aggregator.StaleCompletion: + ".. pii": This model contains a username; the entries are regularly cleaned up (usually every hour) +django_celery_beat.ClockedSchedule: + ".. no_pii:": This model has no PII +django_celery_beat.CrontabSchedule: + ".. no_pii:": This model has no PII +django_celery_beat.IntervalSchedule: + ".. no_pii:": This model has no PII +django_celery_beat.PeriodicTask: + ".. no_pii:": This model has no PII +django_celery_beat.PeriodicTasks: + ".. no_pii:": This model has no PII +django_celery_beat.SolarSchedule: + ".. no_pii:": This model has no PII sessions.Session: ".. no_pii:": This model has no PII social_django.Association: diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py new file mode 100644 index 0000000..b9b355d --- /dev/null +++ b/openedx_certificates/admin.py @@ -0,0 +1,204 @@ +"""Admin page configuration for the openedx-certificates app.""" + +from __future__ import annotations + +import importlib +import inspect +from typing import TYPE_CHECKING, Generator + +from django import forms +from django.contrib import admin +from django.utils.html import format_html +from django_object_actions import DjangoObjectActions, action +from django_reverse_admin import ReverseModelAdmin + +from .models import ( + ExternalCertificate, + ExternalCertificateAsset, + ExternalCertificateCourseConfiguration, + ExternalCertificateType, +) + +if TYPE_CHECKING: # pragma: no cover + from django.http import HttpRequest + from django_celery_beat.models import IntervalSchedule + + +class ExternalCertificateTypeAdminForm(forms.ModelForm): + """Generate a list of available functions for the function fields.""" + + retrieval_func = forms.ChoiceField(choices=[]) + generation_func = forms.ChoiceField(choices=[]) + + @staticmethod + def _available_functions(module: str, prefix: str) -> Generator[tuple[str, str], None, None]: + """ + Import a module and return all functions in it that start with a specific prefix. + + :param module: The name of the module to import. + :param prefix: The prefix of the function names to return. + + :return: A tuple containing the functions that start with the prefix in the module. + """ + # TODO: Implement plugin support for the functions. + _module = importlib.import_module(module) + return ( + (f'{obj.__module__}.{name}', f'{obj.__module__}.{name}') + for name, obj in inspect.getmembers(_module, inspect.isfunction) + if name.startswith(prefix) + ) + + @staticmethod + def _get_docstring_custom_options(func: str) -> str: + """ + Get the docstring of the function and return the "Options:" section. + + :param func: The function to get the docstring for. + :returns: The "Options:" section of the docstring. + """ + try: + docstring = ( + 'Custom options:' + + inspect.getdoc( + getattr( + importlib.import_module(func.rsplit('.', 1)[0]), + func.rsplit('.', 1)[1], + ), + ).split("Options:")[1] + ) + except IndexError: + docstring = ( + 'Custom options are not documented for this function. If you selected a different function, ' + 'you need to save your changes to see an updated docstring.' + ) + # Use pre to preserve the newlines and indentation. + return f'
{docstring}
' + + def __init__(self, *args, **kwargs): + """Initializes the choices for the retrieval and generation function selection fields.""" + super().__init__(*args, **kwargs) + self.fields['retrieval_func'].choices = self._available_functions( + 'openedx_certificates.processors', + 'retrieve_', + ) + if self.instance.retrieval_func: + self.fields['retrieval_func'].help_text = self._get_docstring_custom_options(self.instance.retrieval_func) + self.fields['generation_func'].choices = self._available_functions( + 'openedx_certificates.generators', + 'generate_', + ) + if self.instance.generation_func: + self.fields['generation_func'].help_text = self._get_docstring_custom_options(self.instance.generation_func) + + class Meta: # noqa: D106 + model = ExternalCertificateType + fields = '__all__' # noqa: DJ007 + + +@admin.register(ExternalCertificateType) +class ExternalCertificateTypeAdmin(admin.ModelAdmin): # noqa: D101 + form = ExternalCertificateTypeAdminForm + list_display = ('name', 'retrieval_func', 'generation_func') + + +@admin.register(ExternalCertificateAsset) +class ExternalCertificateAssetAdmin(admin.ModelAdmin): # noqa: D101 + list_display = ('description', 'asset_slug') + prepopulated_fields = {"asset_slug": ("description",)} # noqa: RUF012 + + +@admin.register(ExternalCertificateCourseConfiguration) +class ExternalCertificateCourseConfigurationAdmin(DjangoObjectActions, ReverseModelAdmin): + """ + Admin page for the course-specific certificate configuration for each certificate type. + + It manages the associations between configuration and its corresponding periodic task. + The reverse inline provides a way to manage the periodic task from the configuration page. + """ + + inline_type = 'stacked' + inline_reverse = [ # noqa: RUF012 + ( + 'periodic_task', + {'fields': ['enabled', 'interval', 'crontab', 'clocked', 'start_time', 'expires', 'one_off']}, + ), + ] + list_display = ('course_id', 'certificate_type', 'enabled', 'interval') + search_fields = ('course_id', 'certificate_type__name') + list_filter = ('course_id', 'certificate_type') + + def get_inline_instances( + self, + request: HttpRequest, + obj: ExternalCertificateCourseConfiguration = None, + ) -> list[admin.ModelAdmin]: + """ + Hide inlines on the "Add" view in Django admin, and show them on the "Change" view. + + It differentiates "add" and change "view" based on the requested path because the `obj` parameter can be `None` + in the "Change" view when rendering the inlines. + + :param request: HttpRequest object + :param obj: The object being changed, None for add view + :return: A list of InlineModelAdmin instances to be rendered for add/changing an object + """ + return super().get_inline_instances(request, obj) if '/add/' not in request.path else [] + + def enabled(self, obj: ExternalCertificateCourseConfiguration) -> bool: + """Return the 'enabled' status of the periodic task.""" + return obj.periodic_task.enabled + + enabled.boolean = True + + # noinspection PyMethodMayBeStatic + def interval(self, obj: ExternalCertificateCourseConfiguration) -> IntervalSchedule: + """Return the interval of the certificate generation task.""" + return obj.periodic_task.interval + + def get_readonly_fields(self, _request: HttpRequest, obj: ExternalCertificateCourseConfiguration = None) -> tuple: + """Make the course_id field read-only.""" + if obj: # editing an existing object + return *self.readonly_fields, 'course_id', 'certificate_type' + return self.readonly_fields + + @action(label="Generate certificates") + def generate_certificates(self, _request: HttpRequest, obj: ExternalCertificateCourseConfiguration): + """ + Custom action to generate certificates for the current ExternalCertificateCourse instance. + + Args: + _request: The request object. + obj: The ExternalCertificateCourse instance. + """ + # TODO: Use the celery task instead of the generate_certificates method. + obj.generate_certificates() + + change_actions = ('generate_certificates',) + + +@admin.register(ExternalCertificate) +class ExternalCertificateAdmin(admin.ModelAdmin): # noqa: D101 + list_display = ('user_id', 'user_full_name', 'course_id', 'certificate_type', 'status', 'url') + readonly_fields = ( + 'user_id', + 'user_full_name', + 'course_id', + 'certificate_type', + 'status', + 'url', + 'legacy_id', + 'generation_task_id', + ) + + def get_form(self, request: HttpRequest, obj: ExternalCertificate | None = None, **kwargs) -> forms.ModelForm: + """Hide the download_url field.""" + form = super().get_form(request, obj, **kwargs) + form.base_fields['download_url'].widget = forms.HiddenInput() + return form + + # noinspection PyMethodMayBeStatic + def url(self, obj: ExternalCertificate) -> str: + """Display the download URL as a clickable link.""" + if obj.download_url: + return format_html("{url}", url=obj.download_url) + return "-" diff --git a/openedx_certificates/compat.py b/openedx_certificates/compat.py new file mode 100644 index 0000000..4e1bcf6 --- /dev/null +++ b/openedx_certificates/compat.py @@ -0,0 +1,78 @@ +""" +Proxies and compatibility code for edx-platform features. + +This module moderates access to all edx-platform features allowing for cross-version compatibility code. +It also simplifies running tests outside edx-platform's environment by stubbing these functions in unit tests. +""" +from __future__ import annotations + +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from celery import Celery +from django.conf import settings + +if TYPE_CHECKING: # pragma: no cover + from django.contrib.auth.models import User + from opaque_keys.edx.keys import CourseKey + +# TODO: Do we still need all these pylint disable comments? We switched to ruff. + + +def get_celery_app() -> Celery: + """Get Celery app to reuse configuration and queues.""" + if getattr(settings, "TESTING", False): + # We can ignore this in the testing environment. + return Celery(task_always_eager=True) + + # noinspection PyUnresolvedReferences,PyPackageRequirements + from lms import CELERY_APP + + return CELERY_APP # pragma: no cover + + +def get_course_grading_policy(course_id: CourseKey) -> dict: + """Get the course grading policy from Open edX.""" + # noinspection PyUnresolvedReferences,PyPackageRequirements + from xmodule.modulestore.django import modulestore + + return modulestore().get_course(course_id).grading_policy["GRADER"] + + +def get_course_name(course_id: CourseKey) -> str: + """Get the course name from Open edX.""" + # noinspection PyUnresolvedReferences,PyPackageRequirements + from openedx.core.djangoapps.content.learning_sequences.api import get_course_outline + + course_outline = get_course_outline(course_id) + return (course_outline and course_outline.title) or str(course_id) + + +def get_course_enrollments(course_id: CourseKey) -> list[User]: + """Get the course enrollments from Open edX.""" + # noinspection PyUnresolvedReferences,PyPackageRequirements + from common.djangoapps.student.models import CourseEnrollment + + enrollments = CourseEnrollment.objects.filter(course_id=course_id, is_active=True).select_related('user') + return [enrollment.user for enrollment in enrollments] + + +@contextmanager +def prefetch_course_grades(course_id: CourseKey, users: list[User]): + """Prefetch the course grades from Open edX.""" + # noinspection PyUnresolvedReferences,PyPackageRequirements + from lms.djangoapps.grades.api import clear_prefetched_course_grades, prefetch_course_grades + + prefetch_course_grades(course_id, users) + try: + yield + finally: + clear_prefetched_course_grades(course_id) + + +def get_course_grade_factory(): # noqa: ANN201 + """Get the course grade factory from Open edX.""" + # noinspection PyUnresolvedReferences,PyPackageRequirements + from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory + + return CourseGradeFactory() diff --git a/openedx_certificates/exceptions.py b/openedx_certificates/exceptions.py new file mode 100644 index 0000000..95effe9 --- /dev/null +++ b/openedx_certificates/exceptions.py @@ -0,0 +1,9 @@ +"""Custom exceptions for the openedx-certificates app.""" + + +class AssetNotFoundError(Exception): + """Raised when the asset_slug is not found in the ExternalCertificateAsset model.""" + + +class CertificateGenerationError(Exception): + """Raised when the certificate generation Celery task fails.""" diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py new file mode 100644 index 0000000..7ced968 --- /dev/null +++ b/openedx_certificates/generators.py @@ -0,0 +1,153 @@ +""" +This module provides functions to generate certificates. + +The functions prefixed with `generate_` are automatically detected by the admin page and are used to generate the +certificates for the users. + +We will move this module to an external repository (a plugin). +""" + +from __future__ import annotations + +import io +import logging +from typing import TYPE_CHECKING, Any + +from django.conf import settings +from django.core.files.base import ContentFile +from django.core.files.storage import FileSystemStorage, default_storage +from pypdf import PdfReader, PdfWriter +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.pdfgen import canvas + +from openedx_certificates.compat import get_course_name +from openedx_certificates.models import ExternalCertificateAsset + +log = logging.getLogger(__name__) + +if TYPE_CHECKING: # pragma: no cover + from uuid import UUID + + from django.contrib.auth.models import User + from opaque_keys.edx.keys import CourseKey + + +def _get_user_name(user: User) -> str: + """ + Retrieve the user's name. + + :param user: The user to generate the certificate for. + :return: Username. + """ + return user.profile.name or f"{user.first_name} {user.last_name}" + + +def _register_font(options: dict[str, Any]) -> str: + """ + Register a custom font if specified in options. If not specified, use the default font (Helvetica). + + :param options: A dictionary containing the font. + :returns: The font name. + """ + if font := options.get('font'): + pdfmetrics.registerFont(TTFont(font, ExternalCertificateAsset.get_asset_by_slug(font))) + + return font or 'Helvetica' + + +def _write_text_on_template(template: any, font: str, username: str, course_name: str, options: dict[str, Any]) -> any: + """ + Prepare a new canvas and write the user and course name onto it. + + :param template: Pdf template. + :param font: Font name. + :param username: The name of the user to generate the certificate for. + :param course_name: The name of the course the learner completed. + :param options: A dictionary containing the Y coordinates for name and course name. + :returns: A canvas with written data. + """ + template_width, template_height = template.mediabox[2:] + pdf_canvas = canvas.Canvas(io.BytesIO(), pagesize=(template_width, template_height)) + pdf_canvas.setFont(font, 32) + # Write the learner name. + name_x = (template_width - pdf_canvas.stringWidth(username)) / 2 + name_y = options.get('name_y', 290) + pdf_canvas.drawString(name_x, name_y, username) + # Write the course name. + pdf_canvas.setFont(font, 28) + course_name_x = (template_width - pdf_canvas.stringWidth(course_name)) / 2 + course_name_y = options.get('course_name_y', 220) + pdf_canvas.drawString(course_name_x, course_name_y, course_name) + return pdf_canvas + + +def _save_certificate(certificate: PdfWriter, certificate_uuid: UUID) -> str: + """ + Save the final PDF file to BytesIO and upload it using Django default storage. + + :param certificate: Pdf certificate. + :param certificate_uuid: The UUID of the certificate. + :returns: The URL of the saved certificate. + """ + # Save the final PDF file to BytesIO. + output_path = f'external_certificates/{certificate_uuid}.pdf' + pdf_bytes = io.BytesIO() + certificate.write(pdf_bytes) + pdf_bytes.seek(0) # Rewind to start. + # Upload with Django default storage. + certificate_file = ContentFile(pdf_bytes.read()) + # Delete the file if it already exists. + if default_storage.exists(output_path): + default_storage.delete(output_path) + default_storage.save(output_path, certificate_file) + if isinstance(default_storage, FileSystemStorage): + url = f"{settings.LMS_ROOT_URL}{settings.MEDIA_URL}{output_path}" + else: + url = default_storage.url(output_path) + return url + + +def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: UUID, options: dict[str, Any]) -> str: + """ + Generate a PDF certificate. + + :param course_id: The ID of the course the learner completed. + :param user: The user to generate the certificate for. + :param certificate_uuid: The UUID of the certificate to generate. + :param options: A dictionary containing the following keys: + - template_path: The path to the PDF template file. + - output_path: The path to save the generated certificate PDF file. + :returns: The URL of the saved certificate. + """ + log.info("Starting certificate generation for user %s", user.id) + # Get template from the ExternalCertificateAsset. + template_file = ExternalCertificateAsset.get_asset_by_slug(options['template']) + + username = _get_user_name(user) + course_name = get_course_name(course_id) + + # HACK: We support two-line strings by using a semicolon as a separator. + if ';' in course_name and (template_path := options.get('template_two-lines')): + template_file = ExternalCertificateAsset.get_asset_by_slug(template_path) + course_name = course_name.replace(';', '\n') + + font = _register_font(options) + + # Load the PDF template. + with template_file.open('rb') as template_file: + template = PdfReader(template_file).pages[0] + + certificate = PdfWriter() + + # Create a new canvas, prepare the page and write the data + pdf_canvas = _write_text_on_template(template, font, username, course_name, options) + + overlay_pdf = PdfReader(io.BytesIO(pdf_canvas.getpdfdata())) + template.merge_page(overlay_pdf.pages[0]) + certificate.add_page(template) + + url = _save_certificate(certificate, certificate_uuid) + + log.info("Certificate saved to %s", url) + return url diff --git a/openedx_certificates/migrations/0001_initial.py b/openedx_certificates/migrations/0001_initial.py new file mode 100644 index 0000000..4193fcb --- /dev/null +++ b/openedx_certificates/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# Generated by Django 3.2.23 on 2023-11-14 15:54 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import jsonfield.fields +import model_utils.fields +import opaque_keys.edx.django.models +import openedx_certificates.models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('django_celery_beat', '0018_improve_crontab_helptext'), + ] + + operations = [ + migrations.CreateModel( + name='ExternalCertificateAsset', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('description', models.CharField(blank=True, help_text='Description of the asset.', max_length=255)), + ('asset', models.FileField(help_text='Asset file. It could be a PDF template, image or font file.', max_length=255, upload_to=openedx_certificates.models.ExternalCertificateAsset.template_assets_path)), + ('asset_slug', models.SlugField(help_text="Asset's unique slug. We can reference the asset in templates using this value.", max_length=255, unique=True)), + ], + options={ + 'get_latest_by': 'created', + }, + ), + migrations.CreateModel( + name='ExternalCertificateType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(help_text='Name of the certificate type.', max_length=255, unique=True)), + ('retrieval_func', models.CharField(help_text='A name of the function to retrieve eligible users.', max_length=200)), + ('generation_func', models.CharField(help_text='A name of the function to generate certificates.', max_length=200)), + ('custom_options', jsonfield.fields.JSONField(blank=True, default=dict, help_text='Custom options for the functions.')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ExternalCertificate', + fields=[ + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Auto-generated UUID of the certificate', primary_key=True, serialize=False)), + ('user_id', models.IntegerField(help_text='ID of the user receiving the certificate')), + ('user_full_name', models.CharField(help_text='User receiving the certificate', max_length=255)), + ('course_id', opaque_keys.edx.django.models.CourseKeyField(help_text='ID of a course for which the certificate was issued', max_length=255)), + ('certificate_type', models.CharField(help_text='Type of the certificate', max_length=255)), + ('status', models.CharField(choices=[('generating', 'Generating'), ('available', 'Available'), ('error', 'Error'), ('invalidated', 'Invalidated')], default='generating', help_text='Status of the certificate generation task', max_length=32)), + ('download_url', models.URLField(blank=True, help_text='URL of the generated certificate PDF (e.g., to S3)')), + ('legacy_id', models.IntegerField(help_text='Legacy ID of the certificate imported from another system', null=True)), + ('generation_task_id', models.CharField(help_text='Task ID from the Celery queue', max_length=255)), + ], + options={ + 'unique_together': {('user_id', 'course_id', 'certificate_type')}, + }, + ), + migrations.CreateModel( + name='ExternalCertificateCourseConfiguration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('course_id', opaque_keys.edx.django.models.CourseKeyField(help_text='The ID of the course.', max_length=255)), + ('custom_options', jsonfield.fields.JSONField(blank=True, default=dict, help_text='Custom options for the functions. If specified, they are merged with the options defined in the certificate type.')), + ('certificate_type', models.ForeignKey(help_text='Associated certificate type.', on_delete=django.db.models.deletion.CASCADE, to='openedx_certificates.externalcertificatetype')), + ('periodic_task', models.OneToOneField(help_text='Associated periodic task.', on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.periodictask')), + ], + options={ + 'unique_together': {('course_id', 'certificate_type')}, + }, + ), + ] diff --git a/openedx_certificates/migrations/__init__.py b/openedx_certificates/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index 2d34161..b358961 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -1,5 +1,345 @@ -""" -Database models for openedx_certificates. -""" -# from django.db import models -# from model_utils.models import TimeStampedModel +"""Database models for openedx_certificates.""" +from __future__ import annotations + +import json +import logging +import uuid +from importlib import import_module +from pathlib import Path +from typing import TYPE_CHECKING + +import jsonfield +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_celery_beat.models import IntervalSchedule, PeriodicTask +from model_utils.models import TimeStampedModel +from opaque_keys.edx.django.models import CourseKeyField + +from openedx_certificates.exceptions import AssetNotFoundError, CertificateGenerationError + +if TYPE_CHECKING: # pragma: no cover + from django.core.files import File + from django.db.models import QuerySet + + +log = logging.getLogger(__name__) + + +class ExternalCertificateType(TimeStampedModel): + """ + Model to store global certificate configurations for each type. + + .. no_pii: + """ + + name = models.CharField(max_length=255, unique=True, help_text=_('Name of the certificate type.')) + retrieval_func = models.CharField(max_length=200, help_text=_('A name of the function to retrieve eligible users.')) + generation_func = models.CharField(max_length=200, help_text=_('A name of the function to generate certificates.')) + custom_options = jsonfield.JSONField(default=dict, blank=True, help_text=_('Custom options for the functions.')) + + # TODO: Document how to add custom functions to the certificate generation pipeline. + + def __str__(self): + """Get a string representation of this model's instance.""" + return self.name + + def clean(self): + """Ensure that the `retrieval_func` and `generation_func` exist.""" + for func_field in ['retrieval_func', 'generation_func']: + func_path = getattr(self, func_field) + try: + module_path, func_name = func_path.rsplit('.', 1) + module = import_module(module_path) + getattr(module, func_name) # Will raise AttributeError if the function does not exist. + except ValueError as exc: + raise ValidationError({func_field: "Function path must be in format 'module.function_name'."}) from exc + except (ImportError, AttributeError) as exc: + raise ValidationError( + {func_field: f"The function {func_path} could not be found. Please provide a valid path."}, + ) from exc + + +class ExternalCertificateCourseConfiguration(TimeStampedModel): + """ + Model to store course-specific certificate configurations for each certificate type. + + .. no_pii: + """ + + course_id = CourseKeyField(max_length=255, help_text=_('The ID of the course.')) + certificate_type = models.ForeignKey( + ExternalCertificateType, + on_delete=models.CASCADE, + help_text=_('Associated certificate type.'), + ) + periodic_task = models.OneToOneField( + PeriodicTask, + on_delete=models.CASCADE, + help_text=_('Associated periodic task.'), + ) + custom_options = jsonfield.JSONField( + default=dict, + blank=True, + help_text=_( + 'Custom options for the functions. If specified, they are merged with the options defined in the ' + 'certificate type.', + ), + ) + + class Meta: # noqa: D106 + unique_together = (('course_id', 'certificate_type'),) + + def __str__(self): # noqa: D105 + return f'{self.certificate_type.name} in {self.course_id}' + + def save(self, *args, **kwargs): + """Create a new PeriodicTask every time a new ExternalCertificateCourseConfiguration is created.""" + if self._state.adding: + from openedx_certificates.tasks import generate_certificates_for_course_task # Avoid circular imports. + + schedule, created = IntervalSchedule.objects.get_or_create(every=10, period=IntervalSchedule.DAYS) + self.periodic_task = PeriodicTask.objects.create( + enabled=False, + interval=schedule, + name=f'{self.certificate_type} in {self.course_id}', + task=generate_certificates_for_course_task, + ) + + super().save(*args, **kwargs) + + # Update the args of the PeriodicTask to include the ID of the ExternalCertificateCourseConfiguration. + self.periodic_task.args = json.dumps([self.id]) + self.periodic_task.save() + + # Replace the return type with `QuerySet[Self]` after migrating to Python 3.10+. + @classmethod + def get_enabled_configurations(cls) -> QuerySet[ExternalCertificateCourseConfiguration]: + """ + Get the list of enabled configurations. + + :return: A list of ExternalCertificateCourseConfiguration objects. + """ + return ExternalCertificateCourseConfiguration.objects.filter(periodic_task__enabled=True) + + def generate_certificates(self): + """This method allows manual certificate generation from the Django admin.""" + user_ids = self.get_eligible_user_ids() + log.info("The following users are eligible in %s: %s", self.course_id, user_ids) + filtered_user_ids = self.filter_out_user_ids_with_certificates(user_ids) + log.info("The filtered users eligible in %s: %s", self.course_id, filtered_user_ids) + for user_id in filtered_user_ids: + self.generate_certificate_for_user(user_id) + + def filter_out_user_ids_with_certificates(self, user_ids: list[int]) -> list[int]: + """ + Filter out user IDs that already have a certificate for this course and certificate type. + + :param user_ids: A list of user IDs to filter. + :return: A list of user IDs that either: + 1. Do not have a certificate for this course and certificate type. + 2. Have such a certificate with an error status. + """ + # TODO: Delete this after testing. + return user_ids + users_ids_with_certificates = ExternalCertificate.objects.filter( + models.Q(course_id=self.course_id), + models.Q(certificate_type=self.certificate_type), + ~(models.Q(status=ExternalCertificate.Status.ERROR)), + ).values_list('user_id', flat=True) + + filtered_user_ids_set = set(user_ids) - set(users_ids_with_certificates) + return list(filtered_user_ids_set) + + def get_eligible_user_ids(self) -> list[int]: + """ + Get the list of eligible learners for the given course. + + :return: A list of user IDs. + """ + func_path = self.certificate_type.retrieval_func + module_path, func_name = func_path.rsplit('.', 1) + module = import_module(module_path) + func = getattr(module, func_name) + + custom_options = {**self.certificate_type.custom_options, **self.custom_options} + return func(self.course_id, custom_options) + + def generate_certificate_for_user(self, user_id: int, celery_task_id: int = 0): + """ + Celery task for processing a single user's certificate. + + This function retrieves an ExternalCertificateCourse object based on course_id and certificate_type_id, + retrieves the data using the retrieval_func specified in the associated ExternalCertificateType object, + and passes this data to the function specified in the generation_func field. + + Args: + user_id: The ID of the user to process the certificate for. + celery_task_id (optional): The ID of the Celery task that is running this function. + """ + user = get_user_model().objects.get(id=user_id) + # Use the name from the profile if it is not empty. Otherwise, use the first and last name. + # We check if the profile exists because it is absent in unit tests. + user_full_name = getattr(getattr(user, 'profile', None), 'name', f"{user.first_name} {user.last_name}") + custom_options = {**self.certificate_type.custom_options, **self.custom_options} + + try: + certificate = ExternalCertificate.objects.get( + user_id=user_id, + course_id=self.course_id, + certificate_type=self.certificate_type.name, + ) + certificate.user_full_name = user_full_name + certificate.status = ExternalCertificate.Status.GENERATING + certificate.generation_task_id = celery_task_id + certificate.save() + except ExternalCertificate.DoesNotExist: + certificate = ExternalCertificate.objects.create( + user_id=user_id, + user_full_name=user_full_name, + course_id=self.course_id, + certificate_type=self.certificate_type.name, + status=ExternalCertificate.Status.GENERATING, + generation_task_id=celery_task_id, + ) + + try: + generation_module_name, generation_func_name = self.certificate_type.generation_func.rsplit('.', 1) + generation_module = import_module(generation_module_name) + generation_func = getattr(generation_module, generation_func_name) + + # Run the functions. We do not validate them here, as they are validated in the model's clean() method. + certificate.download_url = generation_func(self.course_id, user, certificate.uuid, custom_options) + certificate.status = ExternalCertificate.Status.AVAILABLE + certificate.save() + except Exception as exc: # noqa: BLE001 + certificate.status = ExternalCertificate.Status.ERROR + certificate.save() + msg = f'Failed to generate the {certificate.uuid=} for {user_id=} with {self.id=}.' + raise CertificateGenerationError(msg) from exc + + +class ExternalCertificate(TimeStampedModel): + """ + Model to represent each individual certificate awarded to a user for a course. + + This model contains information about the related course, the user who earned the certificate, + the download URL for the certificate PDF, and the associated certificate generation task. + + .. note:: The ID field is not a conventional auto-incrementing integer, but a value + that allows for old certificates with custom IDs. + + .. pii: The User's name is stored in this model. + .. pii_types: id, name + .. pii_retirement: retained + """ + + class Status(models.TextChoices): + """Status of the certificate generation task.""" + + GENERATING = 'generating', _('Generating') + AVAILABLE = 'available', _('Available') + ERROR = 'error', _('Error') + INVALIDATED = 'invalidated', _('Invalidated') + + uuid = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + editable=False, + help_text=_('Auto-generated UUID of the certificate'), + ) + user_id = models.IntegerField(help_text=_('ID of the user receiving the certificate')) + user_full_name = models.CharField(max_length=255, help_text=_('User receiving the certificate')) + course_id = CourseKeyField(max_length=255, help_text=_('ID of a course for which the certificate was issued')) + certificate_type = models.CharField(max_length=255, help_text=_('Type of the certificate')) + status = models.CharField( + max_length=32, + choices=Status.choices, + default=Status.GENERATING, + help_text=_('Status of the certificate generation task'), + ) + download_url = models.URLField(blank=True, help_text=_('URL of the generated certificate PDF (e.g., to S3)')) + legacy_id = models.IntegerField(null=True, help_text=_('Legacy ID of the certificate imported from another system')) + generation_task_id = models.CharField(max_length=255, help_text=_('Task ID from the Celery queue')) + + class Meta: # noqa: D106 + unique_together = (('user_id', 'course_id', 'certificate_type'),) + + def __str__(self): # noqa: D105 + return f"{self.certificate_type} for {self.user_full_name} in {self.course_id}" + + +class ExternalCertificateAsset(TimeStampedModel): + """ + A set of assets to be used in custom certificate templates. + + This model stores assets used during certificate generation process, such as PDF templates, images, fonts. + + .. no_pii: + """ + + def template_assets_path(self, filename: str) -> str: + """ + Delete the file if it already exists and returns the certificate template asset file path. + + :param filename: File to upload. + :return path: Path of asset file e.g. `certificate_template_assets/1/filename`. + """ + name = Path('external_certificate_template_assets') / str(self.id) / filename + fullname = Path(settings.MEDIA_ROOT) / name + if fullname.exists(): + fullname.unlink() + return str(name) + + description = models.CharField( + max_length=255, + null=False, + blank=True, + help_text=_('Description of the asset.'), + ) + asset = models.FileField( + max_length=255, + upload_to=template_assets_path, + help_text=_('Asset file. It could be a PDF template, image or font file.'), + ) + asset_slug = models.SlugField( + max_length=255, + unique=True, + null=False, + help_text=_('Asset\'s unique slug. We can reference the asset in templates using this value.'), + ) + + class Meta: # noqa: D106 + get_latest_by = 'created' + + def __str__(self): # noqa: D105 + return f'{self.asset.url}' + + def save(self, *args, **kwargs): + """If the object is being created, save the asset first, then save the object.""" + if self._state.adding: + asset_image = self.asset + self.asset = None + super().save(*args, **kwargs) + self.asset = asset_image + + super().save(*args, **kwargs) + + @classmethod + def get_asset_by_slug(cls, asset_slug: str) -> File: + """ + Fetch a certificate template asset by its slug from the database. + + :param asset_slug: The slug of the asset to be retrieved. + :returns: The file associated with the asset slug. + :raises AssetNotFound: If no asset exists with the provided slug in the ExternalCertificateAsset database model. + """ + try: + template_asset = cls.objects.get(asset_slug=asset_slug) + asset = template_asset.asset + except cls.DoesNotExist as exc: + msg = f'Asset with slug {asset_slug} does not exist.' + raise AssetNotFoundError(msg) from exc + return asset diff --git a/openedx_certificates/pipelines.py b/openedx_certificates/pipelines.py new file mode 100644 index 0000000..8da560d --- /dev/null +++ b/openedx_certificates/pipelines.py @@ -0,0 +1,18 @@ +"""TODO: Add some docstring here. We may also want to move this to a different file.""" + +from django.contrib.auth.models import User +from edx_ace import Message, Recipient, ace + + +def send_email(user: User, certificate_link: str): + """Send a certificate link to the student.""" + msg = Message( + name="Certificate", + app_label="openedx_certificates", + recipient=Recipient(lms_user_id=user.id, email_address=user.email), + language='en', + context={ + 'certificate_link': certificate_link, + }, + ) + ace.send(msg) diff --git a/openedx_certificates/processors.py b/openedx_certificates/processors.py new file mode 100644 index 0000000..d080f00 --- /dev/null +++ b/openedx_certificates/processors.py @@ -0,0 +1,224 @@ +""" +This module contains processors for certificate criteria. + +The functions prefixed with `retrieve_` are automatically detected by the admin page and are used to retrieve the +IDs of the users that meet the criteria for the certificate type. + +We will move this module to an external repository (a plugin). +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from completion_aggregator.api.v1.views import CompletionDetailView +from django.contrib.auth import get_user_model +from rest_framework.request import Request +from rest_framework.test import APIRequestFactory + +from openedx_certificates.compat import ( + get_course_enrollments, + get_course_grade_factory, + get_course_grading_policy, + prefetch_course_grades, +) + +if TYPE_CHECKING: # pragma: no cover + from django.contrib.auth.models import User + from opaque_keys.edx.keys import CourseKey + from rest_framework.views import APIView + + +log = logging.getLogger(__name__) + + +def _get_category_weights(course_id: CourseKey) -> dict[str, float]: + """ + Retrieve the course grading policy and return the weight of each category. + + :param course_id: The course ID to get the grading policy for. + :returns: A dictionary with the weight of each category. + """ + log.debug('Getting the course grading policy.') + grading_policy = get_course_grading_policy(course_id) + log.debug('Finished getting the course grading policy.') + + # Calculate the total weight of the non-exam categories + log.debug(grading_policy) + + category_weight_ratios = {category['type'].lower(): category['weight'] for category in grading_policy} + + log.debug(category_weight_ratios) + return category_weight_ratios + + +def _get_grades_by_format(course_id: CourseKey, users: list[User]) -> dict[int, dict[str, int]]: + """ + Get the grades for each user, categorized by assignment types. + + :param course_id: The course ID. + :param users: The users to get the grades for. + :returns: A dictionary with the grades for each user, categorized by assignment types. + """ + log.debug('Getting the grades for each user.') + + grades = {} + + with prefetch_course_grades(course_id, users): + course_grade_factory = get_course_grade_factory() + + for user in users: + grades[user.id] = {} + course_grade = course_grade_factory.read(user, course_key=course_id) + for assignment_type, subsections in course_grade.graded_subsections_by_format().items(): + assignment_earned = 0 + assignment_possible = 0 + log.debug(subsections) + for subsection in subsections.values(): + assignment_earned += subsection.graded_total.earned + assignment_possible += subsection.graded_total.possible + grade = (assignment_earned / assignment_possible) * 100 if assignment_possible > 0 else 0 + grades[user.id][assignment_type.lower()] = grade + + log.debug('Finished getting the grades for each user.') + return grades + + +def _are_grades_passing_criteria( + user_grades: dict[str, float], + required_grades: dict[str, float], + category_weights: dict[str, float], +) -> bool: + """ + Determine whether the user received passing grades in all required categories. + + :param user_grades: The grades of the user, divided by category. + :param required_grades: The required grades for each category. + :param category_weights: The weight of each category. + :returns: Whether the user received passing grades in all required categories. + :raises ValueError: If a category weight is not found. + """ + # If user does not have a grade for a category (except for the "total" category), it means that they did not + # attempt it. Therefore, they should not be eligible for the certificate. + if not all(category in user_grades for category in required_grades if category != 'total'): + return False + + total_score = 0 + for category, score in user_grades.items(): + if score < required_grades.get(category, 0): + return False + + if category not in category_weights: + msg = "Category weight '%s' was not found in the course grading policy." + raise ValueError(msg, category) + total_score += score * category_weights[category] + + return total_score >= required_grades.get('total', 0) + + +def retrieve_subsection_grades(course_id: CourseKey, options: dict[str, Any]) -> list[int]: + """ + Retrieve the users that have passing grades in all required categories. + + :param course_id: The course ID. + :param options: The custom options for the certificate. + + Options: + - required_grades: A dictionary of required grades for each category, where the keys are the category names and + the values are the minimum required grades. The grades are percentages, so they should be in the range [0, 1]. + See the following example:: + + { + "required_grades": { + "Homework": 0.4, + "Exam": 0.9, + "Total": 0.8 + } + } + + It means that the user must receive at least 40% in the Homework category and 90% in the Exam category. + The "Total" key is a special value used to specify the minimum required grade for all categories in the course. + Let's assume that we have the following grading policy (the percentages are the weights of each category): + 1. Homework: 20% + 2. Lab: 10% + 3. Exam: 70% + The grades for the Total category will be calculated as follows: + total_grade = (homework_grade * 0.2) + (lab_grade * 0.1) + (exam_grade * 0.7) + """ + required_grades: dict[str, int] = options['required_grades'] + required_grades = {key.lower(): value * 100 for key, value in required_grades.items()} + + users = get_course_enrollments(course_id) + grades = _get_grades_by_format(course_id, users) + log.debug(grades) + weights = _get_category_weights(course_id) + + eligible_users = [] + for user_id, user_grades in grades.items(): + if _are_grades_passing_criteria(user_grades, required_grades, weights): + eligible_users.append(user_id) + + return eligible_users + + +def _prepare_request_to_completion_aggregator(course_id: CourseKey, query_params: dict, url: str) -> APIView: + """ + Prepare a request to the Completion Aggregator API. + + :param course_id: The course ID. + :param query_params: The query parameters to use in the request. + :param url: The URL to use in the request. + :returns: The view with the prepared request. + """ + log.debug('Preparing the request for retrieving the completion.') + + # The URL does not matter, as we do not retrieve any data from the path. + django_request = APIRequestFactory().get(url, query_params) + django_request.course_id = course_id + drf_request = Request(django_request) # convert django.core.handlers.wsgi.WSGIRequest to DRF request + + view = CompletionDetailView() + view.request = drf_request + + # HACK: Bypass the API permissions. + staff_user = get_user_model().objects.filter(is_staff=True).first() + view._effective_user = staff_user # noqa: SLF001 + + log.debug('Finished preparing the request for retrieving the completion.') + return view + + +def retrieve_course_completions(course_id: CourseKey, options: dict[str, Any]) -> list[int]: + """ + Retrieve the course completions for all users through the Completion Aggregator API. + + Options: + - required_completion: The minimum required completion percentage. The default value is 0.9. + """ + # If it turns out to be too slow, we can: + # 1. Modify the Completion Aggregator to emit a signal/event when a user achieves a certain completion threshold. + # 2. Get this data from the `Aggregator` model. Filter by `aggregation name == 'course'`, `course_key`, `percent`. + + required_completion = options.get('required_completion', 0.9) + + url = f'/completion-aggregator/v1/course/{course_id}/' + query_params = {'page_size': 1000, 'page': 1} + + # TODO: Extract the logic of this view into an API. The current approach is very hacky. + view = _prepare_request_to_completion_aggregator(course_id, query_params.copy(), url) + completions = [] + + while True: + # noinspection PyUnresolvedReferences + response = view.get(view.request, str(course_id)) + log.debug(response.data) + completions.extend( + res['username'] for res in response.data['results'] if res['completion']['percent'] >= required_completion + ) + if not response.data['pagination']['next']: + break + query_params['page'] += 1 + view = _prepare_request_to_completion_aggregator(course_id, query_params.copy(), url) + + return list(get_user_model().objects.filter(username__in=completions).values_list('id', flat=True)) diff --git a/openedx_certificates/tasks.py b/openedx_certificates/tasks.py new file mode 100644 index 0000000..989b7cb --- /dev/null +++ b/openedx_certificates/tasks.py @@ -0,0 +1,54 @@ +"""Asynchronous Celery tasks.""" + +from __future__ import annotations + +from openedx_certificates.compat import get_celery_app +from openedx_certificates.models import ExternalCertificateCourseConfiguration + +app = get_celery_app() + + +@app.task +def generate_certificate_for_user_task(course_config_id: int, user_id: int): + """ + Celery task for processing a single user's certificate. + + This function retrieves an ExternalCertificateCourse object based on course_id and certificate_type_id, + retrieves the data using the retrieval_func specified in the associated ExternalCertificateType object, + and passes this data to the function specified in the generation_func field. + + :param course_config_id: The ID of the ExternalCertificateCourseConfiguration object to process. + :param user_id: The ID of the user to process the certificate for. + """ + course_config = ExternalCertificateCourseConfiguration.objects.get(id=course_config_id) + course_config.generate_certificate_for_user(user_id, generate_certificate_for_user_task.request.id) + + +@app.task +def generate_certificates_for_course_task(course_config_id: int): + """ + Celery task for processing a single course's certificates. + + This function retrieves an ExternalCertificateCourse object based on course_id and certificate_type_id, + retrieves the data using the retrieval_func specified in the associated ExternalCertificateType object, + and passes this data to the function specified in the generation_func field. + + :param course_config_id: The ID of the ExternalCertificateCourseConfiguration object to process. + """ + course_config = ExternalCertificateCourseConfiguration.objects.get(id=course_config_id) + user_ids = course_config.get_eligible_user_ids() + for name in user_ids: + generate_certificate_for_user_task.apply_async(course_config_id, name) + + +@app.task +def generate_all_certificates_task(): + """ + Celery task for initiating the processing of certificates for all enabled courses. + + This function fetches all enabled ExternalCertificateCourse objects, + and initiates a separate Celery task (process_certificate_for_course) for each of them. + """ + course_config_ids = ExternalCertificateCourseConfiguration.get_enabled_configurations().values_list('id', flat=True) + for config_id in course_config_ids: + generate_certificates_for_course_task.delay(config_id) diff --git a/openedx_certificates/views.py b/openedx_certificates/views.py new file mode 100644 index 0000000..4578657 --- /dev/null +++ b/openedx_certificates/views.py @@ -0,0 +1 @@ +"""TODO.""" diff --git a/requirements/base.in b/requirements/base.in index f747a23..7978064 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -8,6 +8,7 @@ celery # Distributed task queue django-celery-beat # Periodic task scheduler django_reverse_admin # Provides reverse inlines in the admin interface djangorestframework # RESTful API framework +django-object-actions # Provides actions on objects in the admin interface # TODO: Extract these to a plugin. pypdf # PDF manipulation library reportlab # PDF generation library diff --git a/setup.py b/setup.py index 28eeaea..379c315 100755 --- a/setup.py +++ b/setup.py @@ -146,6 +146,11 @@ def is_requirement(line: str) -> bool: include=['openedx_certificates', 'openedx_certificates.*'], exclude=["*tests"], ), + entry_points={ + "lms.djangoapp": [ + "openedx_certificates = openedx_certificates.apps:OpenedxCertificatesConfig", + ], + }, include_package_data=True, install_requires=load_requirements(Path('requirements/base.in')), options={'bdist_wheel': {'universal': True}}, diff --git a/test_settings.py b/test_settings.py index c00338b..fe3b75a 100644 --- a/test_settings.py +++ b/test_settings.py @@ -30,9 +30,18 @@ def root(path: Path) -> Path: 'django.contrib.contenttypes', 'django.contrib.messages', 'django.contrib.sessions', + 'completion', + 'completion_aggregator', + 'django_celery_beat', 'openedx_certificates', + 'django_object_actions', ) +MIGRATION_MODULES = { + # the module 'third_party_app' is the one you want to skip + 'completion_aggregator': None, +} + LOCALE_PATHS = [ root(Path('openedx_certificates/conf/locale')), ] @@ -59,3 +68,5 @@ def root(path: Path) -> Path: }, }, ] + +TESTING = True diff --git a/tests/test_generators.py b/tests/test_generators.py new file mode 100644 index 0000000..774a7dc --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,34 @@ +"""This module contains unit tests for the generate_pdf_certificate function.""" + +import tempfile +import unittest +from pathlib import Path + +from pypdf import PdfReader + +from openedx_certificates.generators import generate_pdf_certificate + + +class TestGeneratePdfCertificate(unittest.TestCase): + """Unit tests for the generate_pdf_certificate function.""" + + def test_generate_pdf_certificate(self): + """Generate a PDF certificate and check that it contains the correct data.""" + data = { + 'username': 'Test user', + 'course_name': 'Some course', + 'template_path': 'openedx_certificates/static/certificate_templates/achievement.pdf', + } + + # Generate the PDF certificate. + with tempfile.NamedTemporaryFile(suffix='.pdf') as certificate_file: + data['output_path'] = certificate_file.name + generate_pdf_certificate(data) + + assert Path(data['output_path']).exists() + + pdf_reader = PdfReader(certificate_file) + page = pdf_reader.pages[0] + text = page.extract_text() + assert data['username'] in text + assert data['course_name'] in text diff --git a/tests/test_models.py b/tests/test_models.py index 834ad4d..7209834 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,19 +1,21 @@ -"""Tests for the `openedx-certificates` models module.""" +"""Tests for the `openedx-certificates` generators module.""" -import pytest +import os +import tempfile +from openedx_certificates.generators import generate_pdf_certificate -class TestExternalCertificateConfiguration: - """Tests of the ExternalCertificateConfiguration model.""" - @pytest.mark.skip(reason="Placeholder to allow pytest to succeed before real tests are in place.") - def test_placeholder(self): - """TODO: Delete this test once there are real tests.""" +class TestGeneratePdfCertificate: + """Tests for the `generate_pdf_certificate` function.""" - -class TestExternalCertificateType: - """Tests of the ExternalCertificateType model.""" - - @pytest.mark.skip(reason="Placeholder to allow pytest to succeed before real tests are in place.") - def test_placeholder(self): - """TODO: Delete this test once there are real tests.""" + def test_generate_pdf_certificate(self): + """Test that the function generates a PDF certificate.""" + data = { + 'username': 'Test user', + 'course_name': 'Some course', + 'template_path': 'openedx_certificates/static/certificate_templates/achievement.pdf', + 'output_path': tempfile.NamedTemporaryFile(suffix='.pdf').name, + } + generate_pdf_certificate(data) + assert os.path.isfile(data['output_path']) diff --git a/tests/test_processors.py b/tests/test_processors.py new file mode 100644 index 0000000..2db151c --- /dev/null +++ b/tests/test_processors.py @@ -0,0 +1,91 @@ +"""This module contains unit tests for the processors module.""" + +import unittest +from unittest.mock import Mock, patch + +import pytest +from opaque_keys.edx.keys import CourseKey + +from openedx_certificates.processors import User, retrieve_course_completions, retrieve_subsection_grades + + +class TestProcessors(unittest.TestCase): + """Unit tests for the processors module.""" + + @pytest.mark.django_db() + @patch('openedx_certificates.processors.get_section_grades_breakdown_view') + @patch('openedx_certificates.processors.get_course_grading_policy') + def test_retrieve_subsection_grades(self, mock_policy: Mock, mock_view: Mock): + """Test that retrieve_subsection_grades returns the expected results.""" + User.objects.create(username='ecommerce_worker', is_staff=True, is_superuser=True) + + course_id = CourseKey.from_string('org/course/run') + user_id = 1 + + # Mock the response from the API view. + mock_response = { + 'results': [ + { + 'username': 'user1', + 'section_breakdown': [ + {'category': 'Category 1', 'detail': 'Detail 1', 'percent': 50}, + {'category': 'Category 1', 'detail': 'Detail 2', 'percent': 50}, + {'category': 'Category 2', 'detail': 'Detail 3', 'percent': 75}, + {'category': 'Category 2', 'detail': 'Detail 4', 'percent': 25}, + {'category': 'Exam', 'detail': 'Detail 5', 'percent': 80}, + ], + }, + { + 'username': 'user2', + 'section_breakdown': [ + {'category': 'Category 1', 'detail': 'Detail 1', 'percent': 100}, + {'category': 'Category 2', 'detail': 'Detail 2', 'percent': 50}, + {'category': 'Exam', 'detail': 'Detail 3', 'percent': 70}, + ], + }, + ], + } + mock_view.return_value.get.return_value.data = mock_response + + # Mock the grading policy. + mock_policy.return_value = [ + {'type': 'Category 1', 'weight': 0.2}, + {'type': 'Category 2', 'weight': 0.3}, + {'type': 'Exam', 'weight': 0.5}, + ] + + # TODO: Fix this. + # This function should return a boolean if the user passed the threshold. + + # Call the function and check the results. + expected_results = { + 'user1': 0.6, + 'user2': 0.65, + } + results = retrieve_subsection_grades(course_id, user_id) + assert results == expected_results + + @pytest.mark.django_db() + @patch('openedx_certificates.processors.CompletionDetailView') + def test_retrieve_course_completions(self, mock_view): + """Test that retrieve_course_completions returns the expected results.""" + User.objects.create(username='ecommerce_worker', is_staff=True, is_superuser=True) + + course_id = CourseKey.from_string('org/course/run') + required_completion = 0.5 + + # Mock the response from the API view. + mock_response = { + 'results': [ + {'username': 'user1', 'completion': {'percent': 0.4}}, + {'username': 'user2', 'completion': {'percent': 0.6}}, + {'username': 'user3', 'completion': {'percent': 0.7}}, + ], + 'pagination': {'next': None}, + } + mock_view.return_value.get.return_value.data = mock_response + + # Call the function and check the results. + expected_results = ['user2', 'user3'] + results = retrieve_course_completions(course_id, required_completion) + assert results == expected_results From 68631e10be1567ed7acac2b1043618198e3d7d64 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 25 Oct 2023 00:25:48 +0200 Subject: [PATCH 10/46] test: add tests --- openedx_certificates/compat.py | 2 - requirements/test.in | 2 + test_settings.py | 3 + test_utils/factories.py | 29 +++ tests/test_generators.py | 229 ++++++++++++++++++--- tests/test_models.py | 275 +++++++++++++++++++++++-- tests/test_processors.py | 361 +++++++++++++++++++++++++-------- 7 files changed, 774 insertions(+), 127 deletions(-) create mode 100644 test_utils/factories.py diff --git a/openedx_certificates/compat.py b/openedx_certificates/compat.py index 4e1bcf6..b367844 100644 --- a/openedx_certificates/compat.py +++ b/openedx_certificates/compat.py @@ -16,8 +16,6 @@ from django.contrib.auth.models import User from opaque_keys.edx.keys import CourseKey -# TODO: Do we still need all these pylint disable comments? We switched to ruff. - def get_celery_app() -> Celery: """Get Celery app to reuse configuration and queues.""" diff --git a/requirements/test.in b/requirements/test.in index 33ec25e..38adc3f 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -7,3 +7,5 @@ pytest-cov # pytest extension for code coverage statistics django-coverage-plugin # https://github.com/nedbat/django_coverage_plugin pytest-django # pytest extension for better Django support code-annotations # provides commands used by the pii_check make target. +dj-inmemorystorage # provides an in-memory storage backend for Django +factory-boy # provides a fixtures replacement for pytest diff --git a/test_settings.py b/test_settings.py index fe3b75a..4cdd747 100644 --- a/test_settings.py +++ b/test_settings.py @@ -64,9 +64,12 @@ def root(path: Path) -> Path: 'context_processors': [ 'django.contrib.auth.context_processors.auth', # this is required for admin 'django.contrib.messages.context_processors.messages', # this is required for admin + 'django.template.context_processors.request', # this is required for admin ], }, }, ] TESTING = True +USE_TZ = True +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/test_utils/factories.py b/test_utils/factories.py new file mode 100644 index 0000000..db9a407 --- /dev/null +++ b/test_utils/factories.py @@ -0,0 +1,29 @@ +"""Factories for creating test data.""" + +from datetime import datetime + +import factory +from django.contrib.auth.models import User +from factory.django import DjangoModelFactory +from pytz import UTC + + +class UserFactory(DjangoModelFactory): + """A Factory for User objects.""" + + class Meta: # noqa: D106 + model = User + django_get_or_create = ('email', 'username') + + _DEFAULT_PASSWORD = 'test' # noqa: S105 + + username = factory.Sequence('robot{}'.format) + email = factory.Sequence('robot+test+{}@edx.org'.format) + password = factory.django.Password(_DEFAULT_PASSWORD) + first_name = factory.Sequence('Robot{}'.format) + last_name = 'Test' + is_staff = False + is_active = True + is_superuser = False + last_login = datetime(2012, 1, 1, tzinfo=UTC) + date_joined = datetime(2011, 1, 1, tzinfo=UTC) diff --git a/tests/test_generators.py b/tests/test_generators.py index 774a7dc..5301b47 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,34 +1,215 @@ """This module contains unit tests for the generate_pdf_certificate function.""" +from __future__ import annotations -import tempfile -import unittest -from pathlib import Path +import io +from unittest.mock import Mock, call, patch +from uuid import uuid4 -from pypdf import PdfReader +import pytest +from django.conf import settings +from django.core.files.base import ContentFile +from django.core.files.storage import DefaultStorage, FileSystemStorage +from django.test import override_settings +from inmemorystorage import InMemoryStorage +from opaque_keys.edx.keys import CourseKey +from pypdf import PdfWriter -from openedx_certificates.generators import generate_pdf_certificate +from openedx_certificates.generators import ( + _get_user_name, + _register_font, + _save_certificate, + _write_text_on_template, + generate_pdf_certificate, +) -class TestGeneratePdfCertificate(unittest.TestCase): - """Unit tests for the generate_pdf_certificate function.""" +def test_get_user_name(): + """Test the _get_user_name function.""" + user = Mock(first_name="First", last_name="Last") + user.profile.name = "Profile Name" - def test_generate_pdf_certificate(self): - """Generate a PDF certificate and check that it contains the correct data.""" - data = { - 'username': 'Test user', - 'course_name': 'Some course', - 'template_path': 'openedx_certificates/static/certificate_templates/achievement.pdf', - } + # Test when profile name is available + assert _get_user_name(user) == "Profile Name" - # Generate the PDF certificate. - with tempfile.NamedTemporaryFile(suffix='.pdf') as certificate_file: - data['output_path'] = certificate_file.name - generate_pdf_certificate(data) + # Test when profile name is not available + user.profile.name = None + assert _get_user_name(user) == "First Last" - assert Path(data['output_path']).exists() - pdf_reader = PdfReader(certificate_file) - page = pdf_reader.pages[0] - text = page.extract_text() - assert data['username'] in text - assert data['course_name'] in text +@patch("openedx_certificates.generators.ExternalCertificateAsset.get_asset_by_slug") +def test_register_font_without_custom_font(mock_get_asset_by_slug: Mock): + """Test the _register_font falls back to the default font when no custom font is specified.""" + options = {} + assert _register_font(options) == "Helvetica" + mock_get_asset_by_slug.assert_not_called() + + +@patch("openedx_certificates.generators.ExternalCertificateAsset.get_asset_by_slug") +@patch('openedx_certificates.generators.TTFont') +@patch("openedx_certificates.generators.pdfmetrics.registerFont") +def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_class: Mock, mock_get_asset_by_slug: Mock): + """Test the _register_font registers the custom font when specified.""" + custom_font = "MyFont" + options = {"font": custom_font} + + mock_get_asset_by_slug.return_value = "font_path" + + assert _register_font(options) == custom_font + mock_get_asset_by_slug.assert_called_once_with(custom_font) + mock_font_class.assert_called_once_with(custom_font, mock_get_asset_by_slug.return_value) + mock_register_font.assert_called_once_with(mock_font_class.return_value) + + +@pytest.mark.parametrize( + ("username", "course_name", "options"), + [ + ('John Doe', 'Programming 101', {}), # No options - use default coordinates. + ('John Doe', 'Programming 101', {'name_y': 250, 'course_name_y': 200}), # Custom coordinates. + ], +) +@patch('openedx_certificates.generators.canvas.Canvas', return_value=Mock(stringWidth=Mock(return_value=10))) +def test_write_text_on_template(mock_canvas_class: Mock, username: str, course_name: str, options: dict[str, int]): + """Test the _write_text_on_template function.""" + template_height = 300 + template_width = 200 + font = 'Helvetica' + string_width = mock_canvas_class.return_value.stringWidth.return_value + + # Reset the mock to discard calls list from previous tests + mock_canvas_class.reset_mock() + + template_mock = Mock() + template_mock.mediabox = [0, 0, template_width, template_height] + + # Call the function with test parameters and mocks + _write_text_on_template(template_mock, font, username, course_name, options) + + # Verifying that Canvas was the correct pagesize. + # Use `call_args_list` to ignore the first argument, which is an instance of io.BytesIO. + assert mock_canvas_class.call_args_list[0][1]['pagesize'] == (template_width, template_height) + + # Mock Canvas object retrieved from Canvas constructor call + canvas_object = mock_canvas_class.return_value + + # Expected coordinates for drawString method, based on fixed stringWidth + expected_name_x = (template_width - string_width) / 2 + expected_name_y = options.get('name_y', 290) + expected_course_name_x = (template_width - string_width) / 2 + expected_course_name_y = options.get('course_name_y', 220) + + # Check the calls to setFont and drawString methods on Canvas object + assert canvas_object.setFont.call_args_list[0] == call(font, 32) + assert canvas_object.drawString.call_args_list[0] == call(expected_name_x, expected_name_y, username) + + assert canvas_object.setFont.call_args_list[1] == call(font, 28) + assert canvas_object.drawString.call_args_list[1] == call( + expected_course_name_x, + expected_course_name_y, + course_name, + ) + + +@override_settings(LMS_ROOT_URL="http://example.com", MEDIA_URL="media/") +@pytest.mark.parametrize( + "storage", + [ + (InMemoryStorage()), # Test a real storage, without mocking. + (Mock(spec=FileSystemStorage, exists=Mock(return_value=False))), # Test calls in a mocked storage. + # Test calls in a mocked storage when the file already exists. + (Mock(spec=FileSystemStorage, exists=Mock(return_value=True))), + ], +) +@patch('openedx_certificates.generators.ContentFile', autospec=True) +def test_save_certificate(mock_contentfile: Mock, storage: DefaultStorage | Mock): + """Test the _save_certificate function.""" + # Mock the certificate. + certificate = Mock(spec=PdfWriter) + certificate_uuid = uuid4() + output_path = f'external_certificates/{certificate_uuid}.pdf' + pdf_bytes = io.BytesIO() + certificate.write.return_value = pdf_bytes + content_file = ContentFile(pdf_bytes.getvalue()) + mock_contentfile.return_value = content_file + + # Run the function. + with patch('openedx_certificates.generators.default_storage', storage): + url = _save_certificate(certificate, certificate_uuid) + + # Check the calls in a mocked storage. + if isinstance(storage, Mock): + storage.exists.assert_called_once_with(output_path) + storage.save.assert_called_once_with(output_path, content_file) + storage.url.assert_not_called() + if storage.exists.return_value: + storage.delete.assert_called_once_with(output_path) + else: + storage.delete.assert_not_called() + + if isinstance(storage, Mock): + assert url == f'{settings.LMS_ROOT_URL}/media/{output_path}' + else: + assert url == f'/{output_path}' + + +@pytest.mark.parametrize( + ("course_name", "options", "expected_template_slug"), + [ + ('Test Course', {'template': 'template_slug'}, 'template_slug'), + ('Test Course;Test Course', {'template': 'template_slug'}, 'template_slug'), + ( + 'Test Course;Test Course', + {'template': 'template_slug', 'template_two-lines': 'template_two_lines_slug'}, + 'template_two_lines_slug', + ), + ], +) +@patch( + 'openedx_certificates.generators.ExternalCertificateAsset.get_asset_by_slug', + return_value=Mock( + open=Mock( + return_value=Mock( + __enter__=Mock(return_value=Mock(read=Mock(return_value=b'pdf_data'))), + __exit__=Mock(return_value=None), + ), + ), + ), +) +@patch('openedx_certificates.generators._get_user_name') +@patch('openedx_certificates.generators.get_course_name') +@patch('openedx_certificates.generators._register_font') +@patch('openedx_certificates.generators.PdfReader') +@patch('openedx_certificates.generators.PdfWriter') +@patch( + 'openedx_certificates.generators._write_text_on_template', + return_value=Mock(getpdfdata=Mock(return_value=b'pdf_data')), +) +@patch('openedx_certificates.generators._save_certificate', return_value='certificate_url') +def test_generate_pdf_certificate( # noqa: PLR0913 + mock_save_certificate: Mock, + mock_write_text_on_template: Mock, + mock_pdf_writer: Mock, + mock_pdf_reader: Mock, + mock_register_font: Mock, + mock_get_course_name: Mock, + mock_get_user_name: Mock, + mock_get_asset_by_slug: Mock, + course_name: str, + options: dict[str, str], + expected_template_slug: str, +): + """Test the generate_pdf_certificate function.""" + course_id = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course') + user = Mock() + mock_get_course_name.return_value = course_name + + result = generate_pdf_certificate(course_id, user, Mock(), options) + + assert result == 'certificate_url' + mock_get_asset_by_slug.assert_called_with(expected_template_slug) + mock_get_user_name.assert_called_once_with(user) + mock_get_course_name.assert_called_once_with(course_id) + mock_register_font.assert_called_once_with(options) + mock_pdf_reader.assert_called() + mock_pdf_writer.assert_called() + mock_write_text_on_template.assert_called_once() + mock_save_certificate.assert_called_once() diff --git a/tests/test_models.py b/tests/test_models.py index 7209834..cf4ed4f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,21 +1,266 @@ -"""Tests for the `openedx-certificates` generators module.""" +"""Tests for the `openedx-certificates` models.""" +from __future__ import annotations -import os -import tempfile +from typing import TYPE_CHECKING, Any +from unittest.mock import Mock, patch +from uuid import UUID, uuid4 -from openedx_certificates.generators import generate_pdf_certificate +import pytest +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from openedx_certificates.exceptions import CertificateGenerationError +from openedx_certificates.models import ( + ExternalCertificate, + ExternalCertificateCourseConfiguration, + ExternalCertificateType, +) +from test_utils.factories import UserFactory -class TestGeneratePdfCertificate: - """Tests for the `generate_pdf_certificate` function.""" +if TYPE_CHECKING: + from django.contrib.auth.models import User + from opaque_keys.edx.keys import CourseKey - def test_generate_pdf_certificate(self): - """Test that the function generates a PDF certificate.""" - data = { - 'username': 'Test user', - 'course_name': 'Some course', - 'template_path': 'openedx_certificates/static/certificate_templates/achievement.pdf', - 'output_path': tempfile.NamedTemporaryFile(suffix='.pdf').name, + +def _mock_retrieval_func(_course_id: CourseKey, _options: dict[str, Any]) -> list[int]: + return [1, 2, 3] + + +def _mock_generation_func(_course_id: CourseKey, _user: User, _certificate_uuid: UUID, _options: dict[str, Any]) -> str: + return "test_url" + + +class TestExternalCertificateType: + """Tests for the ExternalCertificateType model.""" + + def test_str(self): + """Test the string representation of the model.""" + certificate_type = ExternalCertificateType(name="Test Type") + assert str(certificate_type) == "Test Type" + + def test_clean_with_valid_functions(self): + """Test the clean method with valid function paths.""" + certificate_type = ExternalCertificateType( + name="Test Type", + retrieval_func="test_models._mock_retrieval_func", + generation_func="test_models._mock_generation_func", + ) + certificate_type.clean() + + @pytest.mark.parametrize("function_path", ["", "invalid_format_func"]) + def test_clean_with_invalid_function_format(self, function_path: str): + """Test the clean method with invalid function format.""" + certificate_type = ExternalCertificateType( + name="Test Type", + retrieval_func="test_models._mock_retrieval_func", + generation_func=function_path, + ) + with pytest.raises(ValidationError) as exc: + certificate_type.clean() + assert "Function path must be in format 'module.function_name'" in str(exc.value) + + def test_clean_with_invalid_function(self): + """Test the clean method with invalid function paths.""" + certificate_type = ExternalCertificateType( + name="Test Type", + retrieval_func="test_models._mock_retrieval_func", + generation_func="invalid.module.path", + ) + with pytest.raises(ValidationError) as exc: + certificate_type.clean() + assert ( + f"The function {certificate_type.generation_func} could not be found. Please provide a valid path" + in str(exc.value) + ) + + +class TestExternalCertificateCourseConfiguration: + """Tests for the ExternalCertificateCourseConfiguration model.""" + + def setup_method(self): + """Prepare the test data.""" + self.certificate_type = ExternalCertificateType( + name="Test Type", + retrieval_func="test_models._mock_retrieval_func", + generation_func="test_models._mock_generation_func", + ) + self.course_config = ExternalCertificateCourseConfiguration( + course_id="course-v1:TestX+T101+2023", + certificate_type=self.certificate_type, + ) + + @pytest.mark.django_db() + def test_periodic_task_is_auto_created(self): + """Test that a periodic task is automatically created for the new configuration.""" + self.certificate_type.save() + self.course_config.save() + self.course_config.refresh_from_db() + + assert (periodic_task := self.course_config.periodic_task) is not None + assert periodic_task.enabled is False + assert periodic_task.name == str(self.course_config) + assert periodic_task.args == f'[{self.course_config.id}]' + + def test_str_representation(self): + """Test the string representation of the model.""" + assert str(self.course_config) == f'{self.certificate_type.name} in course-v1:TestX+T101+2023' + + def test_get_eligible_user_ids(self): + """Test the get_eligible_user_ids method.""" + eligible_user_ids = self.course_config.get_eligible_user_ids() + assert eligible_user_ids == [1, 2, 3] + + @pytest.mark.xfail(reason="The filtering is currently disabled for testing purposes.") + @pytest.mark.django_db() + def test_filter_out_user_ids_with_certificates(self): + """Test the filter_out_user_ids_with_certificates method.""" + self.certificate_type.save() + self.course_config.save() + + cert_data = { + "course_id": self.course_config.course_id, + "certificate_type": self.certificate_type.name, + } + + ExternalCertificate.objects.create( + uuid=uuid4(), + user_id=1, + status=ExternalCertificate.Status.GENERATING, + **cert_data, + ) + ExternalCertificate.objects.create( + uuid=uuid4(), + user_id=2, + status=ExternalCertificate.Status.AVAILABLE, + **cert_data, + ) + ExternalCertificate.objects.create( + uuid=uuid4(), + user_id=3, + status=ExternalCertificate.Status.ERROR, + **cert_data, + ) + ExternalCertificate.objects.create( + uuid=uuid4(), + user_id=4, + status=ExternalCertificate.Status.INVALIDATED, + **cert_data, + ) + ExternalCertificate.objects.create( + uuid=uuid4(), + user_id=5, + status=ExternalCertificate.Status.ERROR, + **cert_data, + ) + + filtered_users = self.course_config.filter_out_user_ids_with_certificates([1, 2, 3, 4, 6]) + assert filtered_users == [3, 6] + + @pytest.mark.django_db() + def test_generate_certificate_for_user(self): + """Test the generate_certificate_for_user method.""" + user = UserFactory.create() + task_id = 123 + + self.course_config.generate_certificate_for_user(user.id, task_id) + assert ExternalCertificate.objects.filter( + user_id=user.id, + course_id=self.course_config.course_id, + certificate_type=self.certificate_type, + user_full_name=f"{user.first_name} {user.last_name}", + status=ExternalCertificate.Status.AVAILABLE, + generation_task_id=task_id, + download_url="test_url", + ).exists() + + @pytest.mark.django_db() + def test_generate_certificate_for_user_update_existing(self): + """Test the generate_certificate_for_user method updates an existing certificate.""" + user = UserFactory.create() + + ExternalCertificate.objects.create( + user_id=user.id, + course_id=self.course_config.course_id, + certificate_type=self.certificate_type, + user_full_name="Random Name", + status=ExternalCertificate.Status.ERROR, + generation_task_id=123, + download_url="random_url", + ) + + self.course_config.generate_certificate_for_user(user.id) + assert ExternalCertificate.objects.filter( + user_id=user.id, + course_id=self.course_config.course_id, + certificate_type=self.certificate_type, + user_full_name=f"{user.first_name} {user.last_name}", + status=ExternalCertificate.Status.AVAILABLE, + generation_task_id=0, + download_url="test_url", + ).exists() + + @pytest.mark.django_db() + @patch('openedx_certificates.models.import_module') + def test_generate_certificate_for_user_with_exception(self, mock_module: Mock): + """Test the generate_certificate_for_user handles the case when the generation function raises an exception.""" + user = UserFactory.create() + task_id = 123 + + def mock_func_raise_exception(*_args, **_kwargs): + msg = "Test Exception" + raise RuntimeError(msg) + + mock_module.return_value = mock_func_raise_exception + + # Call the method under test and check that it raises the correct exception. + with pytest.raises(CertificateGenerationError) as exc: + self.course_config.generate_certificate_for_user(user.id, task_id) + + assert 'Failed to generate the' in str(exc.value) + assert ExternalCertificate.objects.filter( + user_id=user.id, + course_id=self.course_config.course_id, + certificate_type=self.certificate_type, + user_full_name=f"{user.first_name} {user.last_name}", + status=ExternalCertificate.Status.ERROR, + generation_task_id=task_id, + download_url='', + ).exists() + + +class TestExternalCertificate: + """Tests for the ExternalCertificate model.""" + + def setup_method(self): + """Prepare the test data.""" + self.certificate = ExternalCertificate( + uuid=uuid4(), + user_id=1, + user_full_name='Test User', + course_id='course-v1:TestX+T101+2023', + certificate_type='Test Type', + status=ExternalCertificate.Status.GENERATING, + download_url='http://www.test.com', + generation_task_id='12345', + ) + + def test_str_representation(self): + """Test the string representation of a certificate.""" + assert str(self.certificate) == 'Test Type for Test User in course-v1:TestX+T101+2023' + + @pytest.mark.django_db() + def test_unique_together_constraint(self): + """Test that the unique_together constraint is enforced.""" + self.certificate.save() + certificate_2_info = { + "uuid": uuid4(), + "user_id": 1, + "user_full_name": 'Test User 2', + "course_id": 'course-v1:TestX+T101+2023', + "certificate_type": 'Test Type', + "status": ExternalCertificate.Status.GENERATING, + "download_url": 'http://www.test2.com', + "generation_task_id": '122345', } - generate_pdf_certificate(data) - assert os.path.isfile(data['output_path']) + with pytest.raises(IntegrityError): + ExternalCertificate.objects.create(**certificate_2_info) diff --git a/tests/test_processors.py b/tests/test_processors.py index 2db151c..4ab9068 100644 --- a/tests/test_processors.py +++ b/tests/test_processors.py @@ -1,91 +1,280 @@ -"""This module contains unit tests for the processors module.""" +"""Tests for the certificate processors.""" +from __future__ import annotations -import unittest -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch import pytest +from django.http import QueryDict from opaque_keys.edx.keys import CourseKey -from openedx_certificates.processors import User, retrieve_course_completions, retrieve_subsection_grades - - -class TestProcessors(unittest.TestCase): - """Unit tests for the processors module.""" - - @pytest.mark.django_db() - @patch('openedx_certificates.processors.get_section_grades_breakdown_view') - @patch('openedx_certificates.processors.get_course_grading_policy') - def test_retrieve_subsection_grades(self, mock_policy: Mock, mock_view: Mock): - """Test that retrieve_subsection_grades returns the expected results.""" - User.objects.create(username='ecommerce_worker', is_staff=True, is_superuser=True) - - course_id = CourseKey.from_string('org/course/run') - user_id = 1 - - # Mock the response from the API view. - mock_response = { - 'results': [ - { - 'username': 'user1', - 'section_breakdown': [ - {'category': 'Category 1', 'detail': 'Detail 1', 'percent': 50}, - {'category': 'Category 1', 'detail': 'Detail 2', 'percent': 50}, - {'category': 'Category 2', 'detail': 'Detail 3', 'percent': 75}, - {'category': 'Category 2', 'detail': 'Detail 4', 'percent': 25}, - {'category': 'Exam', 'detail': 'Detail 5', 'percent': 80}, - ], - }, - { - 'username': 'user2', - 'section_breakdown': [ - {'category': 'Category 1', 'detail': 'Detail 1', 'percent': 100}, - {'category': 'Category 2', 'detail': 'Detail 2', 'percent': 50}, - {'category': 'Exam', 'detail': 'Detail 3', 'percent': 70}, - ], - }, - ], - } - mock_view.return_value.get.return_value.data = mock_response - - # Mock the grading policy. - mock_policy.return_value = [ - {'type': 'Category 1', 'weight': 0.2}, - {'type': 'Category 2', 'weight': 0.3}, - {'type': 'Exam', 'weight': 0.5}, - ] - - # TODO: Fix this. - # This function should return a boolean if the user passed the threshold. - - # Call the function and check the results. - expected_results = { - 'user1': 0.6, - 'user2': 0.65, - } - results = retrieve_subsection_grades(course_id, user_id) - assert results == expected_results - - @pytest.mark.django_db() - @patch('openedx_certificates.processors.CompletionDetailView') - def test_retrieve_course_completions(self, mock_view): - """Test that retrieve_course_completions returns the expected results.""" - User.objects.create(username='ecommerce_worker', is_staff=True, is_superuser=True) - - course_id = CourseKey.from_string('org/course/run') - required_completion = 0.5 - - # Mock the response from the API view. - mock_response = { - 'results': [ - {'username': 'user1', 'completion': {'percent': 0.4}}, - {'username': 'user2', 'completion': {'percent': 0.6}}, - {'username': 'user3', 'completion': {'percent': 0.7}}, - ], - 'pagination': {'next': None}, - } - mock_view.return_value.get.return_value.data = mock_response - - # Call the function and check the results. - expected_results = ['user2', 'user3'] - results = retrieve_course_completions(course_id, required_completion) - assert results == expected_results +# noinspection PyProtectedMember +from openedx_certificates.processors import ( + _are_grades_passing_criteria, + _get_category_weights, + _get_grades_by_format, + _prepare_request_to_completion_aggregator, + retrieve_course_completions, + retrieve_subsection_grades, +) + + +@patch( + 'openedx_certificates.processors.get_course_grading_policy', + return_value=[{'type': 'Homework', 'weight': 0.15}, {'type': 'Exam', 'weight': 0.85}], +) +def test_get_category_weights(mock_get_course_grading_policy: Mock): + """Check that the course grading policy is retrieved and the category weights are calculated correctly.""" + course_id = Mock(spec=CourseKey) + assert _get_category_weights(course_id) == {'homework': 0.15, 'exam': 0.85} + mock_get_course_grading_policy.assert_called_once_with(course_id) + + +@patch('openedx_certificates.processors.prefetch_course_grades') +@patch('openedx_certificates.processors.get_course_grade_factory') +def test_get_grades_by_format(mock_get_course_grade_factory: Mock, mock_prefetch_course_grades: Mock): + """Test that grades are retrieved for each user and categorized by assignment types.""" + course_id = Mock(spec=CourseKey) + users = [Mock(name="User1", id=101), Mock(name="User2", id=102)] + + mock_read_grades = Mock() + mock_read_grades.return_value.graded_subsections_by_format.return_value = { + 'Homework': {'subsection1': Mock(graded_total=Mock(earned=50.0, possible=100.0))}, + 'Exam': {'subsection2': Mock(graded_total=Mock(earned=90.0, possible=100.0))}, + } + mock_get_course_grade_factory.return_value.read = mock_read_grades + + result = _get_grades_by_format(course_id, users) + + assert result == {101: {'homework': 50.0, 'exam': 90.0}, 102: {'homework': 50.0, 'exam': 90.0}} + mock_prefetch_course_grades.assert_called_once_with(course_id, users) + mock_get_course_grade_factory.assert_called_once() + + mock_read_grades.assert_has_calls( + [ + call(users[0], course_key=course_id), + call().graded_subsections_by_format(), + call(users[1], course_key=course_id), + call().graded_subsections_by_format(), + ], + ) + + +_are_grades_passing_criteria_test_data = [ + ( + "All grades are passing", + {"homework": 90, "lab": 90, "exam": 90}, + {"homework": 85, "lab": 80, "exam": 60, "total": 50}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + True, + ), + ( + "The homework grade is failing", + {"homework": 80, "lab": 90, "exam": 70}, + {"homework": 85, "lab": 80, "exam": 60, "total": 50}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + False, + ), + ( + "The total grade is failing", + {"homework": 90, "lab": 90, "exam": 70}, + {"homework": 85, "lab": 80, "exam": 60, "total": 300}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + False, + ), + ( + "Only the total grade is required", + {"homework": 90, "lab": 90, "exam": 70}, + {"total": 50}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + True, + ), + ( + "Total grade is not required", + {"homework": 90, "lab": 90, "exam": 70}, + {"homework": 85, "lab": 80}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + True, + ), + ( + "Required grades are not defined", + {"homework": 80, "lab": 90, "exam": 70}, + {}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + True, + ), + ( + "User has no grades", + {}, + {"homework": 85, "lab": 80, "exam": 60, "total": 240}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + False, + ), + ("User has no grades and the required grades are not defined", {}, {}, {}, True), + ( + "User has no grades in a required category", + {"homework": 90, "lab": 85}, + {"homework": 85, "lab": 80, "exam": 60}, + {"homework": 0.3, "lab": 0.3, "exam": 0.4}, + False, + ), +] + + +@pytest.mark.parametrize( + ('desc', 'user_grades', 'required_grades', 'category_weights', 'expected'), + _are_grades_passing_criteria_test_data, + ids=[i[0] for i in _are_grades_passing_criteria_test_data], +) +def test_are_grades_passing_criteria( + desc: str, # noqa: ARG001 + user_grades: dict[str, float], + required_grades: dict[str, float], + category_weights: dict[str, float], + expected: bool, # noqa: FBT001 +): + """Test that the user grades are compared to the required grades correctly.""" + assert _are_grades_passing_criteria(user_grades, required_grades, category_weights) == expected + + +def test_are_grades_passing_criteria_invalid_grade_category(): + """Test that an exception is raised if user grades contain a category that is not defined in the grading policy.""" + with pytest.raises(ValueError, match='unknown_category'): + _are_grades_passing_criteria( + {"homework": 90, "unknown_category": 90}, + {"total": 175}, + {"homework": 0.5, "lab": 0.5}, + ) + + +@patch('openedx_certificates.processors.get_course_enrollments') +@patch('openedx_certificates.processors._get_grades_by_format') +@patch('openedx_certificates.processors._get_category_weights') +@patch('openedx_certificates.processors._are_grades_passing_criteria') +def test_retrieve_subsection_grades( + mock_are_grades_passing_criteria: Mock, + mock_get_category_weights: Mock, + mock_get_grades_by_format: Mock, + mock_get_course_enrollments: Mock, +): + """Test that the function returns the eligible users.""" + course_id = Mock(spec=CourseKey) + options = { + 'required_grades': { + 'homework': 0.4, + 'exam': 0.9, + 'total': 0.8, + }, + } + users = [Mock(name="User1", id=101), Mock(name="User2", id=102)] + grades = { + 101: {'homework': 0.5, 'exam': 0.9}, + 102: {'homework': 0.3, 'exam': 0.95}, + } + required_grades = {'homework': 40.0, 'exam': 90.0, 'total': 80.0} + weights = {'homework': 0.2, 'exam': 0.7, 'lab': 0.1} + + mock_get_course_enrollments.return_value = users + mock_get_grades_by_format.return_value = grades + mock_get_category_weights.return_value = weights + mock_are_grades_passing_criteria.side_effect = [True, False] + + result = retrieve_subsection_grades(course_id, options) + + assert result == [101] + mock_get_course_enrollments.assert_called_once_with(course_id) + mock_get_grades_by_format.assert_called_once_with(course_id, users) + mock_get_category_weights.assert_called_once_with(course_id) + mock_are_grades_passing_criteria.assert_has_calls( + [ + call(grades[101], required_grades, weights), + call(grades[102], required_grades, weights), + ], + ) + + +def test_prepare_request_to_completion_aggregator(): + """Test that the request to the completion aggregator API is prepared correctly.""" + course_id = Mock(spec=CourseKey) + query_params = {'param1': 'value1', 'param2': 'value2'} + url = '/test_url/' + + with patch('openedx_certificates.processors.get_user_model') as mock_get_user_model, patch( + 'openedx_certificates.processors.CompletionDetailView', + ) as mock_view_class: + staff_user = Mock(is_staff=True) + mock_get_user_model().objects.filter().first.return_value = staff_user + + view = _prepare_request_to_completion_aggregator(course_id, query_params, url) + + mock_view_class.assert_called_once() + assert view.request.course_id == course_id + # noinspection PyUnresolvedReferences + assert view._effective_user is staff_user + assert isinstance(view, mock_view_class.return_value.__class__) + + # Create a QueryDict from the query_params dictionary. + query_params_qdict = QueryDict('', mutable=True) + query_params_qdict.update(query_params) + assert view.request.query_params.urlencode() == query_params_qdict.urlencode() + + +@patch('openedx_certificates.processors._prepare_request_to_completion_aggregator') +@patch('openedx_certificates.processors.get_user_model') +def test_retrieve_course_completions(mock_get_user_model: Mock, mock_prepare_request_to_completion_aggregator: Mock): + """Test that we retrieve the course completions for all users and return IDs of users who meet the criteria.""" + course_id = Mock(spec=CourseKey) + options = {'required_completion': 0.8} + completions_page1 = { + 'pagination': {'next': '/completion-aggregator/v1/course/{course_id}/?page=2&page_size=1000'}, + 'results': [ + {'username': 'user1', 'completion': {'percent': 0.9}}, + ], + } + completions_page2 = { + 'pagination': {'next': None}, + 'results': [ + {'username': 'user2', 'completion': {'percent': 0.7}}, + {'username': 'user3', 'completion': {'percent': 0.8}}, + ], + } + + mock_view_page1 = Mock() + mock_view_page1.get.return_value.data = completions_page1 + mock_view_page2 = Mock() + mock_view_page2.get.return_value.data = completions_page2 + mock_prepare_request_to_completion_aggregator.side_effect = [mock_view_page1, mock_view_page2] + + def filter_side_effect(*_args, **kwargs) -> list[int]: + """ + A mock side effect function for User.objects.filter(). + + It allows testing this code without a database access. + + :returns: The user IDs corresponding to the provided usernames. + """ + usernames = kwargs['username__in'] + + values_list_mock = Mock() + values_list_mock.return_value = [username_id_map[username] for username in usernames] + queryset_mock = Mock() + queryset_mock.values_list = values_list_mock + + return queryset_mock + + username_id_map = {"user1": 1, "user2": 2, "user3": 3} + mock_user_model = Mock() + mock_user_model.objects.filter.side_effect = filter_side_effect + mock_get_user_model.return_value = mock_user_model + + result = retrieve_course_completions(course_id, options) + + assert result == [1, 3] + mock_prepare_request_to_completion_aggregator.assert_has_calls( + [ + call(course_id, {'page_size': 1000, 'page': 1}, f'/completion-aggregator/v1/course/{course_id}/'), + call(course_id, {'page_size': 1000, 'page': 2}, f'/completion-aggregator/v1/course/{course_id}/'), + ], + ) + mock_view_page1.get.assert_called_once_with(mock_view_page1.request, str(course_id)) + mock_view_page2.get.assert_called_once_with(mock_view_page2.request, str(course_id)) + mock_user_model.objects.filter.assert_called_once_with(username__in=['user1', 'user3']) From 6181e4e870be68b8d975f6f774ef3ff69b8ea6be Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 15 Nov 2023 17:25:25 +0100 Subject: [PATCH 11/46] fixup! chore: update requirements --- requirements/base.txt | 6 +++--- requirements/ci.txt | 1 + requirements/dev.txt | 9 +++++---- requirements/doc.txt | 8 ++++---- requirements/quality.txt | 9 +++++---- requirements/test.txt | 8 ++++---- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 7566169..211f56b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -118,7 +118,7 @@ edx-django-utils==5.8.0 # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.0 +edx-drf-extensions==8.13.1 # via edx-completion edx-opaque-keys==2.5.1 # via @@ -159,7 +159,7 @@ pbr==6.0.0 # via stevedore pillow==10.1.0 # via reportlab -prompt-toolkit==3.0.40 +prompt-toolkit==3.0.41 # via click-repl psutil==5.9.6 # via edx-django-utils @@ -176,7 +176,7 @@ pymongo==3.13.0 # event-tracking pynacl==1.5.0 # via edx-django-utils -pypdf==3.17.0 +pypdf==3.17.1 # via -r requirements/base.in python-crontab==3.0.0 # via django-celery-beat diff --git a/requirements/ci.txt b/requirements/ci.txt index c2a3c5e..dc227d3 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -16,6 +16,7 @@ packaging==23.2 # tox platformdirs==3.11.0 # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -c requirements/constraints.txt # tox # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index 17dc3a2..c7540b5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -188,7 +188,7 @@ edx-django-utils==5.8.0 # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.0 +edx-drf-extensions==8.13.1 # via # -r requirements/quality.txt # edx-completion @@ -215,7 +215,7 @@ exceptiongroup==1.1.3 # pytest factory-boy==3.3.0 # via -r requirements/quality.txt -faker==20.0.0 +faker==20.0.3 # via # -r requirements/quality.txt # factory-boy @@ -306,6 +306,7 @@ pip-tools==7.3.0 # via -r requirements/pip-tools.txt platformdirs==3.11.0 # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -c requirements/constraints.txt # -r requirements/ci.txt # -r requirements/quality.txt @@ -320,7 +321,7 @@ pluggy==1.3.0 # tox polib==1.2.0 # via edx-i18n-tools -prompt-toolkit==3.0.40 +prompt-toolkit==3.0.41 # via # -r requirements/quality.txt # click-repl @@ -353,7 +354,7 @@ pynacl==1.5.0 # via # -r requirements/quality.txt # edx-django-utils -pypdf==3.17.0 +pypdf==3.17.1 # via -r requirements/quality.txt pyproject-hooks==1.0.0 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index 5e758c7..1ba8286 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -192,7 +192,7 @@ edx-django-utils==5.8.0 # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.0 +edx-drf-extensions==8.13.1 # via # -r requirements/test.txt # edx-completion @@ -217,7 +217,7 @@ exceptiongroup==1.1.3 # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==20.0.0 +faker==20.0.3 # via # -r requirements/test.txt # factory-boy @@ -313,7 +313,7 @@ pluggy==1.3.0 # via # -r requirements/test.txt # pytest -prompt-toolkit==3.0.40 +prompt-toolkit==3.0.41 # via # -r requirements/test.txt # click-repl @@ -350,7 +350,7 @@ pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pypdf==3.17.0 +pypdf==3.17.1 # via -r requirements/test.txt pyproject-hooks==1.0.0 # via build diff --git a/requirements/quality.txt b/requirements/quality.txt index 3541b06..453ade3 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -173,7 +173,7 @@ edx-django-utils==5.8.0 # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.0 +edx-drf-extensions==8.13.1 # via # -r requirements/test.txt # edx-completion @@ -198,7 +198,7 @@ exceptiongroup==1.1.3 # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==20.0.0 +faker==20.0.3 # via # -r requirements/test.txt # factory-boy @@ -267,13 +267,14 @@ pillow==10.1.0 # reportlab platformdirs==3.11.0 # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -c requirements/constraints.txt # black pluggy==1.3.0 # via # -r requirements/test.txt # pytest -prompt-toolkit==3.0.40 +prompt-toolkit==3.0.41 # via # -r requirements/test.txt # click-repl @@ -300,7 +301,7 @@ pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pypdf==3.17.0 +pypdf==3.17.1 # via -r requirements/test.txt pytest==7.4.3 # via diff --git a/requirements/test.txt b/requirements/test.txt index 72d931f..a930a95 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -169,7 +169,7 @@ edx-django-utils==5.8.0 # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.0 +edx-drf-extensions==8.13.1 # via # -r requirements/base.txt # edx-completion @@ -192,7 +192,7 @@ exceptiongroup==1.1.3 # via pytest factory-boy==3.3.0 # via -r requirements/test.in -faker==20.0.0 +faker==20.0.3 # via factory-boy fs==2.4.16 # via @@ -248,7 +248,7 @@ pillow==10.1.0 # reportlab pluggy==1.3.0 # via pytest -prompt-toolkit==3.0.40 +prompt-toolkit==3.0.41 # via # -r requirements/base.txt # click-repl @@ -275,7 +275,7 @@ pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils -pypdf==3.17.0 +pypdf==3.17.1 # via -r requirements/base.txt pytest==7.4.3 # via From 127df6456b9dbeefa169c71567d06e8b73de1ebc Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 15 Nov 2023 17:26:53 +0100 Subject: [PATCH 12/46] chore: upgrade tox from `v3` to `v4` and remove `tox-battery` --- requirements/ci.in | 1 - requirements/ci.txt | 16 ++++++++-------- requirements/dev.txt | 32 ++++++++++++++++++++------------ tox.ini | 4 ++-- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/requirements/ci.in b/requirements/ci.in index 3797849..3586cbe 100644 --- a/requirements/ci.in +++ b/requirements/ci.in @@ -3,4 +3,3 @@ -c constraints.txt tox # Virtualenv management for tests -tox-battery # Makes tox aware of requirements file changes diff --git a/requirements/ci.txt b/requirements/ci.txt index dc227d3..a9f0168 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -4,6 +4,12 @@ # # make upgrade # +cachetools==5.3.2 + # via tox +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via tox distlib==0.3.7 # via virtualenv filelock==3.13.1 @@ -22,19 +28,13 @@ platformdirs==3.11.0 # virtualenv pluggy==1.3.0 # via tox -py==1.11.0 - # via tox -six==1.16.0 +pyproject-api==1.6.1 # via tox tomli==2.0.1 # via # pyproject-api # tox -tox==3.28.0 - # via - # -r requirements/ci.in - # tox-battery -tox-battery==0.6.2 +tox==4.11.3 # via -r requirements/ci.in virtualenv==20.24.6 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index c7540b5..0401118 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -38,6 +38,10 @@ build==1.0.3 # via # -r requirements/pip-tools.txt # pip-tools +cachetools==5.3.2 + # via + # -r requirements/ci.txt + # tox celery==5.3.5 # via # -r requirements/quality.txt @@ -55,7 +59,10 @@ cffi==1.16.0 # cryptography # pynacl chardet==5.2.0 - # via diff-cover + # via + # -r requirements/ci.txt + # diff-cover + # tox charset-normalizer==3.3.2 # via # -r requirements/quality.txt @@ -88,6 +95,10 @@ code-annotations==1.5.0 # via # -r requirements/quality.txt # edx-toggles +colorama==0.4.6 + # via + # -r requirements/ci.txt + # tox coverage[toml]==7.3.2 # via # -r requirements/quality.txt @@ -285,6 +296,7 @@ packaging==23.2 # -r requirements/quality.txt # black # build + # pyproject-api # pytest # tox path==16.7.1 @@ -311,6 +323,7 @@ platformdirs==3.11.0 # -r requirements/ci.txt # -r requirements/quality.txt # black + # tox # virtualenv pluggy==1.3.0 # via @@ -329,10 +342,6 @@ psutil==5.9.6 # via # -r requirements/quality.txt # edx-django-utils -py==1.11.0 - # via - # -r requirements/ci.txt - # tox pycparser==2.21 # via # -r requirements/quality.txt @@ -356,6 +365,10 @@ pynacl==1.5.0 # edx-django-utils pypdf==3.17.1 # via -r requirements/quality.txt +pyproject-api==1.6.1 + # via + # -r requirements/ci.txt + # tox pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt @@ -424,7 +437,6 @@ simplejson==3.19.2 # xblock six==1.16.0 # via - # -r requirements/ci.txt # -r requirements/quality.txt # dj-inmemorystorage # edx-ace @@ -432,7 +444,6 @@ six==1.16.0 # fs # openedx-completion-aggregator # python-dateutil - # tox sqlparse==0.4.4 # via # -r requirements/quality.txt @@ -457,14 +468,11 @@ tomli==2.0.1 # build # coverage # pip-tools + # pyproject-api # pyproject-hooks # pytest # tox -tox==3.28.0 - # via - # -r requirements/ci.txt - # tox-battery -tox-battery==0.6.2 +tox==4.11.3 # via -r requirements/ci.txt typing-extensions==4.8.0 # via diff --git a/tox.ini b/tox.ini index 70f04a0..2cde08d 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,7 @@ setenv = PYTHONPATH = {toxinidir} # Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by sphinx. SPHINXOPTS = -W -whitelist_externals = +allowlist_externals = make rm deps = @@ -59,7 +59,7 @@ commands = twine check dist/* [testenv:quality] -whitelist_externals = +allowlist_externals = make deps = -r{toxinidir}/requirements/quality.txt From 16f9878b68748876424e6c00644f0f4c677ee3fe Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 15 Nov 2023 19:18:44 +0100 Subject: [PATCH 13/46] fixup! feat: initial implementation --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index cf51d52..b16b1f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,3 +9,4 @@ omit = */static/* */templates/* */tests/* + */settings/* From be1e58ed2364611fc9ba4759737fb6c9ff3b3983 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 15 Nov 2023 19:19:09 +0100 Subject: [PATCH 14/46] fixup! feat: initial implementation --- openedx_certificates/apps.py | 8 +++++++- openedx_certificates/settings/__init__.py | 1 + openedx_certificates/settings/common.py | 8 ++++++++ openedx_certificates/settings/production.py | 12 ++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 openedx_certificates/settings/__init__.py create mode 100644 openedx_certificates/settings/common.py create mode 100644 openedx_certificates/settings/production.py diff --git a/openedx_certificates/apps.py b/openedx_certificates/apps.py index 5997f54..851c9ac 100644 --- a/openedx_certificates/apps.py +++ b/openedx_certificates/apps.py @@ -13,4 +13,10 @@ class OpenedxCertificatesConfig(AppConfig): name = 'openedx_certificates' # https://edx.readthedocs.io/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html - plugin_app: ClassVar[dict[str, dict[str, dict]]] = {} + plugin_app: ClassVar[dict[str, dict[str, dict]]] = { + 'settings_config': { + 'lms.djangoapp': { + 'common': {'relative_path': 'settings.common'}, + }, + }, + } diff --git a/openedx_certificates/settings/__init__.py b/openedx_certificates/settings/__init__.py new file mode 100644 index 0000000..355c51b --- /dev/null +++ b/openedx_certificates/settings/__init__.py @@ -0,0 +1 @@ +"""App-specific settings.""" diff --git a/openedx_certificates/settings/common.py b/openedx_certificates/settings/common.py new file mode 100644 index 0000000..ec7424f --- /dev/null +++ b/openedx_certificates/settings/common.py @@ -0,0 +1,8 @@ +"""App-specific settings for all environments.""" +from django.conf import Settings + + +def plugin_settings(settings: Settings): + """Add `django_celery_beat` to `INSTALLED_APPS`.""" + if 'django_celery_beat' not in settings.INSTALLED_APPS: + settings.INSTALLED_APPS += ('django_celery_beat',) diff --git a/openedx_certificates/settings/production.py b/openedx_certificates/settings/production.py new file mode 100644 index 0000000..430e6cf --- /dev/null +++ b/openedx_certificates/settings/production.py @@ -0,0 +1,12 @@ +"""App-specific settings for production environments.""" +from django.conf import Settings + + +def plugin_settings(settings: Settings): + """ + Use the database scheduler for Celery Beat. + + The default scheduler is celery.beat.PersistentScheduler, which stores the schedule in a local file. It does not + work in a multi-server environment, so we use the database scheduler instead. + """ + settings.CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' From 25d985b788e0652e18fce939b609f4c05d063b67 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 15 Nov 2023 19:26:26 +0100 Subject: [PATCH 15/46] fixup! feat: initial implementation --- docs/getting_started.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 929795d..fd0870e 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -66,3 +66,11 @@ Deploying ********* TODO: Document this. + +Ansible Playbooks +================= + +If you still use the `configuration`_ repository to deploy your Open edX instance, set +``EDXAPP_ENABLE_CELERY_BEAT: true`` to enable the Celery beat service. Without this, periodic tasks will not be run. + +.. _configuration: https://github.com/openedx/configuration From a3edc9b17f882ef5bffbc5894d980667a8057078 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 15 Nov 2023 19:26:35 +0100 Subject: [PATCH 16/46] fixup! test: add tests --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f8ab970..ca27db4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ pip-log.txt coverage.xml htmlcov/ pii_report/ +default.db From a083d4f290c513c0a9f638140b9a699b65ca3eb2 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 15 Nov 2023 19:27:13 +0100 Subject: [PATCH 17/46] fixup! feat: initial implementation --- openedx_certificates/apps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openedx_certificates/apps.py b/openedx_certificates/apps.py index 851c9ac..dc65faf 100644 --- a/openedx_certificates/apps.py +++ b/openedx_certificates/apps.py @@ -17,6 +17,7 @@ class OpenedxCertificatesConfig(AppConfig): 'settings_config': { 'lms.djangoapp': { 'common': {'relative_path': 'settings.common'}, + 'production': {'relative_path': 'settings.production'}, }, }, } From 8ca21bb4907cd71e105ba4d47385a57461a4111a Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 10 Jan 2024 23:48:24 +0100 Subject: [PATCH 18/46] chore: update requirements --- requirements/base.txt | 44 +++++++++++----------- requirements/ci.txt | 7 ++-- requirements/dev.txt | 75 +++++++++++++++++++------------------- requirements/doc.txt | 68 ++++++++++++++++++---------------- requirements/pip-tools.txt | 4 +- requirements/pip.txt | 6 +-- requirements/quality.txt | 61 ++++++++++++++++--------------- requirements/test.txt | 54 ++++++++++++++------------- 8 files changed, 166 insertions(+), 153 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 211f56b..229dfdd 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,7 +10,7 @@ appdirs==1.4.4 # via fs asgiref==3.7.2 # via django -attrs==23.1.0 +attrs==23.2.0 # via edx-ace backports-zoneinfo[tzdata]==0.2.1 # via @@ -20,19 +20,21 @@ backports-zoneinfo[tzdata]==0.2.1 # kombu billiard==4.2.0 # via celery -celery==5.3.5 +celery==5.3.6 # via # -r requirements/base.in # django-celery-beat # edx-celeryutils # event-tracking # openedx-completion-aggregator -certifi==2023.7.22 +certifi==2023.11.17 # via requests cffi==1.16.0 # via # cryptography # pynacl +chardet==5.2.0 + # via reportlab charset-normalizer==3.3.2 # via requests click==8.1.7 @@ -53,7 +55,7 @@ code-annotations==1.5.0 # via edx-toggles cron-descriptor==1.4.0 # via django-celery-beat -cryptography==41.0.5 +cryptography==41.0.7 # via pyjwt django==3.2.23 # via @@ -91,9 +93,9 @@ django-object-actions==4.2.0 # via -r requirements/base.in django-reverse-admin==2.9.6 # via -r requirements/base.in -django-timezone-field==6.0.1 +django-timezone-field==6.1.0 # via django-celery-beat -django-waffle==4.0.0 +django-waffle==4.1.0 # via # edx-django-utils # edx-drf-extensions @@ -113,12 +115,12 @@ edx-celeryutils==1.2.3 # via openedx-completion-aggregator edx-completion==4.4.0 # via openedx-completion-aggregator -edx-django-utils==5.8.0 +edx-django-utils==5.9.0 # via # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.1 +edx-drf-extensions==9.1.2 # via edx-completion edx-opaque-keys==2.5.1 # via @@ -134,15 +136,15 @@ event-tracking==2.2.0 # via edx-completion fs==2.4.16 # via xblock -idna==3.4 +idna==3.6 # via requests jinja2==3.1.2 # via code-annotations jsonfield==3.1.0 # via edx-celeryutils -kombu==5.3.3 +kombu==5.3.4 # via celery -lxml==4.9.3 +lxml==5.1.0 # via xblock mako==1.3.0 # via xblock @@ -151,17 +153,17 @@ markupsafe==2.1.3 # jinja2 # mako # xblock -newrelic==9.1.2 +newrelic==9.4.0 # via edx-django-utils openedx-completion-aggregator==4.0.3 # via -r requirements/base.in pbr==6.0.0 # via stevedore -pillow==10.1.0 +pillow==10.2.0 # via reportlab -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via click-repl -psutil==5.9.6 +psutil==5.9.7 # via edx-django-utils pycparser==2.21 # via cffi @@ -176,7 +178,7 @@ pymongo==3.13.0 # event-tracking pynacl==1.5.0 # via edx-django-utils -pypdf==3.17.1 +pypdf==3.17.4 # via -r requirements/base.in python-crontab==3.0.0 # via django-celery-beat @@ -199,7 +201,7 @@ pyyaml==6.0.1 # via # code-annotations # xblock -reportlab==4.0.7 +reportlab==4.0.9 # via -r requirements/base.in requests==2.31.0 # via @@ -230,13 +232,13 @@ stevedore==5.1.0 # edx-opaque-keys text-unidecode==1.3 # via python-slugify -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # asgiref # edx-opaque-keys # kombu # pypdf -tzdata==2023.3 +tzdata==2023.4 # via # backports-zoneinfo # celery @@ -248,13 +250,13 @@ vine==5.1.0 # amqp # celery # kombu -wcwidth==0.2.10 +wcwidth==0.2.13 # via prompt-toolkit web-fragments==2.1.0 # via xblock webob==1.8.7 # via xblock -xblock==1.8.1 +xblock==1.9.1 # via # edx-completion # openedx-completion-aggregator diff --git a/requirements/ci.txt b/requirements/ci.txt index a9f0168..7d1eff5 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -10,7 +10,7 @@ chardet==5.2.0 # via tox colorama==0.4.6 # via tox -distlib==0.3.7 +distlib==0.3.8 # via virtualenv filelock==3.13.1 # via @@ -22,7 +22,6 @@ packaging==23.2 # tox platformdirs==3.11.0 # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -c requirements/constraints.txt # tox # virtualenv @@ -34,7 +33,7 @@ tomli==2.0.1 # via # pyproject-api # tox -tox==4.11.3 +tox==4.11.4 # via -r requirements/ci.in -virtualenv==20.24.6 +virtualenv==20.25.0 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 0401118..d992571 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -16,7 +16,7 @@ asgiref==3.7.2 # via # -r requirements/quality.txt # django -attrs==23.1.0 +attrs==23.2.0 # via # -r requirements/quality.txt # edx-ace @@ -32,7 +32,7 @@ billiard==4.2.0 # via # -r requirements/quality.txt # celery -black==23.11.0 +black==23.12.1 # via -r requirements/quality.txt build==1.0.3 # via @@ -42,14 +42,14 @@ cachetools==5.3.2 # via # -r requirements/ci.txt # tox -celery==5.3.5 +celery==5.3.6 # via # -r requirements/quality.txt # django-celery-beat # edx-celeryutils # event-tracking # openedx-completion-aggregator -certifi==2023.7.22 +certifi==2023.11.17 # via # -r requirements/quality.txt # requests @@ -61,7 +61,9 @@ cffi==1.16.0 chardet==5.2.0 # via # -r requirements/ci.txt + # -r requirements/quality.txt # diff-cover + # reportlab # tox charset-normalizer==3.3.2 # via @@ -99,7 +101,7 @@ colorama==0.4.6 # via # -r requirements/ci.txt # tox -coverage[toml]==7.3.2 +coverage[toml]==7.4.0 # via # -r requirements/quality.txt # coverage @@ -109,13 +111,13 @@ cron-descriptor==1.4.0 # via # -r requirements/quality.txt # django-celery-beat -cryptography==41.0.5 +cryptography==41.0.7 # via # -r requirements/quality.txt # pyjwt -diff-cover==8.0.1 +diff-cover==8.0.2 # via -r requirements/dev.in -distlib==0.3.7 +distlib==0.3.8 # via # -r requirements/ci.txt # virtualenv @@ -162,11 +164,11 @@ django-object-actions==4.2.0 # via -r requirements/quality.txt django-reverse-admin==2.9.6 # via -r requirements/quality.txt -django-timezone-field==6.0.1 +django-timezone-field==6.1.0 # via # -r requirements/quality.txt # django-celery-beat -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/quality.txt # edx-django-utils @@ -193,13 +195,13 @@ edx-completion==4.4.0 # via # -r requirements/quality.txt # openedx-completion-aggregator -edx-django-utils==5.8.0 +edx-django-utils==5.9.0 # via # -r requirements/quality.txt # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.1 +edx-drf-extensions==9.1.2 # via # -r requirements/quality.txt # edx-completion @@ -220,13 +222,13 @@ event-tracking==2.2.0 # via # -r requirements/quality.txt # edx-completion -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 # via # -r requirements/quality.txt # pytest factory-boy==3.3.0 # via -r requirements/quality.txt -faker==20.0.3 +faker==22.1.0 # via # -r requirements/quality.txt # factory-boy @@ -239,11 +241,11 @@ fs==2.4.16 # via # -r requirements/quality.txt # xblock -idna==3.4 +idna==3.6 # via # -r requirements/quality.txt # requests -importlib-metadata==6.8.0 +importlib-metadata==7.0.1 # via # -r requirements/pip-tools.txt # build @@ -260,11 +262,11 @@ jsonfield==3.1.0 # via # -r requirements/quality.txt # edx-celeryutils -kombu==5.3.3 +kombu==5.3.4 # via # -r requirements/quality.txt # celery -lxml==4.9.3 +lxml==5.1.0 # via # -r requirements/quality.txt # edx-i18n-tools @@ -283,7 +285,7 @@ mypy-extensions==1.0.0 # via # -r requirements/quality.txt # black -newrelic==9.1.2 +newrelic==9.4.0 # via # -r requirements/quality.txt # edx-django-utils @@ -299,9 +301,9 @@ packaging==23.2 # pyproject-api # pytest # tox -path==16.7.1 +path==16.9.0 # via edx-i18n-tools -pathspec==0.11.2 +pathspec==0.12.1 # via # -r requirements/quality.txt # black @@ -310,7 +312,7 @@ pbr==6.0.0 # via # -r requirements/quality.txt # stevedore -pillow==10.1.0 +pillow==10.2.0 # via # -r requirements/quality.txt # reportlab @@ -318,7 +320,6 @@ pip-tools==7.3.0 # via -r requirements/pip-tools.txt platformdirs==3.11.0 # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -c requirements/constraints.txt # -r requirements/ci.txt # -r requirements/quality.txt @@ -334,11 +335,11 @@ pluggy==1.3.0 # tox polib==1.2.0 # via edx-i18n-tools -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/quality.txt # click-repl -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/quality.txt # edx-django-utils @@ -346,7 +347,7 @@ pycparser==2.21 # via # -r requirements/quality.txt # cffi -pygments==2.16.1 +pygments==2.17.2 # via diff-cover pyjwt[crypto]==2.8.0 # via @@ -363,7 +364,7 @@ pynacl==1.5.0 # via # -r requirements/quality.txt # edx-django-utils -pypdf==3.17.1 +pypdf==3.17.4 # via -r requirements/quality.txt pyproject-api==1.6.1 # via @@ -373,7 +374,7 @@ pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt # build -pytest==7.4.3 +pytest==7.4.4 # via # -r requirements/quality.txt # pytest-cov @@ -413,14 +414,14 @@ pyyaml==6.0.1 # edx-i18n-tools # xblock # yamllint -reportlab==4.0.7 +reportlab==4.0.9 # via -r requirements/quality.txt requests==2.31.0 # via # -r requirements/quality.txt # edx-drf-extensions # sailthru-client -ruff==0.1.5 +ruff==0.1.11 # via -r requirements/quality.txt sailthru-client==2.2.3 # via @@ -472,9 +473,9 @@ tomli==2.0.1 # pyproject-hooks # pytest # tox -tox==4.11.3 +tox==4.11.4 # via -r requirements/ci.txt -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/quality.txt # asgiref @@ -483,7 +484,7 @@ typing-extensions==4.8.0 # faker # kombu # pypdf -tzdata==2023.3 +tzdata==2023.4 # via # -r requirements/quality.txt # backports-zoneinfo @@ -499,11 +500,11 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.24.6 +virtualenv==20.25.0 # via # -r requirements/ci.txt # tox -wcwidth==0.2.10 +wcwidth==0.2.13 # via # -r requirements/quality.txt # prompt-toolkit @@ -515,11 +516,11 @@ webob==1.8.7 # via # -r requirements/quality.txt # xblock -wheel==0.41.3 +wheel==0.42.0 # via # -r requirements/pip-tools.txt # pip-tools -xblock==1.8.1 +xblock==1.9.1 # via # -r requirements/quality.txt # edx-completion diff --git a/requirements/doc.txt b/requirements/doc.txt index 1ba8286..23e6b4d 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -20,11 +20,11 @@ asgiref==3.7.2 # via # -r requirements/test.txt # django -attrs==23.1.0 +attrs==23.2.0 # via # -r requirements/test.txt # edx-ace -babel==2.13.1 +babel==2.14.0 # via # pydata-sphinx-theme # sphinx @@ -44,14 +44,14 @@ billiard==4.2.0 # celery build==1.0.3 # via -r requirements/doc.in -celery==5.3.5 +celery==5.3.6 # via # -r requirements/test.txt # django-celery-beat # edx-celeryutils # event-tracking # openedx-completion-aggregator -certifi==2023.7.22 +certifi==2023.11.17 # via # -r requirements/test.txt # requests @@ -60,6 +60,10 @@ cffi==1.16.0 # -r requirements/test.txt # cryptography # pynacl +chardet==5.2.0 + # via + # -r requirements/test.txt + # reportlab charset-normalizer==3.3.2 # via # -r requirements/test.txt @@ -89,7 +93,7 @@ code-annotations==1.5.0 # via # -r requirements/test.txt # edx-toggles -coverage[toml]==7.3.2 +coverage[toml]==7.4.0 # via # -r requirements/test.txt # coverage @@ -99,7 +103,7 @@ cron-descriptor==1.4.0 # via # -r requirements/test.txt # django-celery-beat -cryptography==41.0.5 +cryptography==41.0.7 # via # -r requirements/test.txt # pyjwt @@ -146,11 +150,11 @@ django-object-actions==4.2.0 # via -r requirements/test.txt django-reverse-admin==2.9.6 # via -r requirements/test.txt -django-timezone-field==6.0.1 +django-timezone-field==6.1.0 # via # -r requirements/test.txt # django-celery-beat -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/test.txt # edx-django-utils @@ -186,13 +190,13 @@ edx-completion==4.4.0 # via # -r requirements/test.txt # openedx-completion-aggregator -edx-django-utils==5.8.0 +edx-django-utils==5.9.0 # via # -r requirements/test.txt # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.1 +edx-drf-extensions==9.1.2 # via # -r requirements/test.txt # edx-completion @@ -211,13 +215,13 @@ event-tracking==2.2.0 # via # -r requirements/test.txt # edx-completion -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 # via # -r requirements/test.txt # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==20.0.3 +faker==22.1.0 # via # -r requirements/test.txt # factory-boy @@ -225,13 +229,13 @@ fs==2.4.16 # via # -r requirements/test.txt # xblock -idna==3.4 +idna==3.6 # via # -r requirements/test.txt # requests imagesize==1.4.1 # via sphinx -importlib-metadata==6.8.0 +importlib-metadata==7.0.1 # via # build # keyring @@ -260,11 +264,11 @@ jsonfield==3.1.0 # edx-celeryutils keyring==24.3.0 # via twine -kombu==5.3.3 +kombu==5.3.4 # via # -r requirements/test.txt # celery -lxml==4.9.3 +lxml==5.1.0 # via # -r requirements/test.txt # xblock @@ -282,13 +286,13 @@ markupsafe==2.1.3 # xblock mdurl==0.1.2 # via markdown-it-py -more-itertools==10.1.0 +more-itertools==10.2.0 # via jaraco-classes -newrelic==9.1.2 +newrelic==9.4.0 # via # -r requirements/test.txt # edx-django-utils -nh3==0.2.14 +nh3==0.2.15 # via readme-renderer openedx-completion-aggregator==4.0.3 # via -r requirements/test.txt @@ -303,7 +307,7 @@ pbr==6.0.0 # via # -r requirements/test.txt # stevedore -pillow==10.1.0 +pillow==10.2.0 # via # -r requirements/test.txt # reportlab @@ -313,11 +317,11 @@ pluggy==1.3.0 # via # -r requirements/test.txt # pytest -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/test.txt # click-repl -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/test.txt # edx-django-utils @@ -325,9 +329,9 @@ pycparser==2.21 # via # -r requirements/test.txt # cffi -pydata-sphinx-theme==0.14.3 +pydata-sphinx-theme==0.14.4 # via sphinx-book-theme -pygments==2.16.1 +pygments==2.17.2 # via # accessible-pygments # doc8 @@ -350,11 +354,11 @@ pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pypdf==3.17.1 +pypdf==3.17.4 # via -r requirements/test.txt pyproject-hooks==1.0.0 # via build -pytest==7.4.3 +pytest==7.4.4 # via # -r requirements/test.txt # pytest-cov @@ -395,7 +399,7 @@ pyyaml==6.0.1 # xblock readme-renderer==42.0 # via twine -reportlab==4.0.7 +reportlab==4.0.9 # via -r requirements/test.txt requests==2.31.0 # via @@ -411,7 +415,7 @@ restructuredtext-lint==1.4.0 # via doc8 rfc3986==2.0.0 # via twine -rich==13.6.0 +rich==13.7.0 # via twine sailthru-client==2.2.3 # via @@ -486,7 +490,7 @@ tomli==2.0.1 # pytest twine==4.0.2 # via -r requirements/doc.in -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/test.txt # asgiref @@ -496,7 +500,7 @@ typing-extensions==4.8.0 # pydata-sphinx-theme # pypdf # rich -tzdata==2023.3 +tzdata==2023.4 # via # -r requirements/test.txt # backports-zoneinfo @@ -513,7 +517,7 @@ vine==5.1.0 # amqp # celery # kombu -wcwidth==0.2.10 +wcwidth==0.2.13 # via # -r requirements/test.txt # prompt-toolkit @@ -525,7 +529,7 @@ webob==1.8.7 # via # -r requirements/test.txt # xblock -xblock==1.8.1 +xblock==1.9.1 # via # -r requirements/test.txt # edx-completion diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index ea34731..0e88226 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -8,7 +8,7 @@ build==1.0.3 # via pip-tools click==8.1.7 # via pip-tools -importlib-metadata==6.8.0 +importlib-metadata==7.0.1 # via build packaging==23.2 # via build @@ -21,7 +21,7 @@ tomli==2.0.1 # build # pip-tools # pyproject-hooks -wheel==0.41.3 +wheel==0.42.0 # via pip-tools zipp==3.17.0 # via importlib-metadata diff --git a/requirements/pip.txt b/requirements/pip.txt index 9014f2c..a4cf530 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -4,11 +4,11 @@ # # make upgrade # -wheel==0.41.3 +wheel==0.42.0 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==23.3.1 +pip==23.3.2 # via -r requirements/pip.in -setuptools==68.2.2 +setuptools==69.0.3 # via -r requirements/pip.in diff --git a/requirements/quality.txt b/requirements/quality.txt index 453ade3..ed95d7b 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -16,7 +16,7 @@ asgiref==3.7.2 # via # -r requirements/test.txt # django -attrs==23.1.0 +attrs==23.2.0 # via # -r requirements/test.txt # edx-ace @@ -32,16 +32,16 @@ billiard==4.2.0 # via # -r requirements/test.txt # celery -black==23.11.0 +black==23.12.1 # via -r requirements/quality.in -celery==5.3.5 +celery==5.3.6 # via # -r requirements/test.txt # django-celery-beat # edx-celeryutils # event-tracking # openedx-completion-aggregator -certifi==2023.7.22 +certifi==2023.11.17 # via # -r requirements/test.txt # requests @@ -50,6 +50,10 @@ cffi==1.16.0 # -r requirements/test.txt # cryptography # pynacl +chardet==5.2.0 + # via + # -r requirements/test.txt + # reportlab charset-normalizer==3.3.2 # via # -r requirements/test.txt @@ -80,7 +84,7 @@ code-annotations==1.5.0 # via # -r requirements/test.txt # edx-toggles -coverage[toml]==7.3.2 +coverage[toml]==7.4.0 # via # -r requirements/test.txt # coverage @@ -90,7 +94,7 @@ cron-descriptor==1.4.0 # via # -r requirements/test.txt # django-celery-beat -cryptography==41.0.5 +cryptography==41.0.7 # via # -r requirements/test.txt # pyjwt @@ -136,11 +140,11 @@ django-object-actions==4.2.0 # via -r requirements/test.txt django-reverse-admin==2.9.6 # via -r requirements/test.txt -django-timezone-field==6.0.1 +django-timezone-field==6.1.0 # via # -r requirements/test.txt # django-celery-beat -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/test.txt # edx-django-utils @@ -167,13 +171,13 @@ edx-completion==4.4.0 # via # -r requirements/test.txt # openedx-completion-aggregator -edx-django-utils==5.8.0 +edx-django-utils==5.9.0 # via # -r requirements/test.txt # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.1 +edx-drf-extensions==9.1.2 # via # -r requirements/test.txt # edx-completion @@ -192,13 +196,13 @@ event-tracking==2.2.0 # via # -r requirements/test.txt # edx-completion -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 # via # -r requirements/test.txt # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==20.0.3 +faker==22.1.0 # via # -r requirements/test.txt # factory-boy @@ -206,7 +210,7 @@ fs==2.4.16 # via # -r requirements/test.txt # xblock -idna==3.4 +idna==3.6 # via # -r requirements/test.txt # requests @@ -222,11 +226,11 @@ jsonfield==3.1.0 # via # -r requirements/test.txt # edx-celeryutils -kombu==5.3.3 +kombu==5.3.4 # via # -r requirements/test.txt # celery -lxml==4.9.3 +lxml==5.1.0 # via # -r requirements/test.txt # xblock @@ -242,7 +246,7 @@ markupsafe==2.1.3 # xblock mypy-extensions==1.0.0 # via black -newrelic==9.1.2 +newrelic==9.4.0 # via # -r requirements/test.txt # edx-django-utils @@ -253,7 +257,7 @@ packaging==23.2 # -r requirements/test.txt # black # pytest -pathspec==0.11.2 +pathspec==0.12.1 # via # black # yamllint @@ -261,24 +265,23 @@ pbr==6.0.0 # via # -r requirements/test.txt # stevedore -pillow==10.1.0 +pillow==10.2.0 # via # -r requirements/test.txt # reportlab platformdirs==3.11.0 # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -c requirements/constraints.txt # black pluggy==1.3.0 # via # -r requirements/test.txt # pytest -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/test.txt # click-repl -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/test.txt # edx-django-utils @@ -301,9 +304,9 @@ pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pypdf==3.17.1 +pypdf==3.17.4 # via -r requirements/test.txt -pytest==7.4.3 +pytest==7.4.4 # via # -r requirements/test.txt # pytest-cov @@ -342,14 +345,14 @@ pyyaml==6.0.1 # code-annotations # xblock # yamllint -reportlab==4.0.7 +reportlab==4.0.9 # via -r requirements/test.txt requests==2.31.0 # via # -r requirements/test.txt # edx-drf-extensions # sailthru-client -ruff==0.1.5 +ruff==0.1.11 # via -r requirements/quality.in sailthru-client==2.2.3 # via @@ -394,7 +397,7 @@ tomli==2.0.1 # black # coverage # pytest -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/test.txt # asgiref @@ -403,7 +406,7 @@ typing-extensions==4.8.0 # faker # kombu # pypdf -tzdata==2023.3 +tzdata==2023.4 # via # -r requirements/test.txt # backports-zoneinfo @@ -419,7 +422,7 @@ vine==5.1.0 # amqp # celery # kombu -wcwidth==0.2.10 +wcwidth==0.2.13 # via # -r requirements/test.txt # prompt-toolkit @@ -431,7 +434,7 @@ webob==1.8.7 # via # -r requirements/test.txt # xblock -xblock==1.8.1 +xblock==1.9.1 # via # -r requirements/test.txt # edx-completion diff --git a/requirements/test.txt b/requirements/test.txt index a930a95..f8c2c9e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -16,7 +16,7 @@ asgiref==3.7.2 # via # -r requirements/base.txt # django -attrs==23.1.0 +attrs==23.2.0 # via # -r requirements/base.txt # edx-ace @@ -32,14 +32,14 @@ billiard==4.2.0 # via # -r requirements/base.txt # celery -celery==5.3.5 +celery==5.3.6 # via # -r requirements/base.txt # django-celery-beat # edx-celeryutils # event-tracking # openedx-completion-aggregator -certifi==2023.7.22 +certifi==2023.11.17 # via # -r requirements/base.txt # requests @@ -48,6 +48,10 @@ cffi==1.16.0 # -r requirements/base.txt # cryptography # pynacl +chardet==5.2.0 + # via + # -r requirements/base.txt + # reportlab charset-normalizer==3.3.2 # via # -r requirements/base.txt @@ -78,7 +82,7 @@ code-annotations==1.5.0 # -r requirements/base.txt # -r requirements/test.in # edx-toggles -coverage[toml]==7.3.2 +coverage[toml]==7.4.0 # via # coverage # django-coverage-plugin @@ -87,7 +91,7 @@ cron-descriptor==1.4.0 # via # -r requirements/base.txt # django-celery-beat -cryptography==41.0.5 +cryptography==41.0.7 # via # -r requirements/base.txt # pyjwt @@ -132,11 +136,11 @@ django-object-actions==4.2.0 # via -r requirements/base.txt django-reverse-admin==2.9.6 # via -r requirements/base.txt -django-timezone-field==6.0.1 +django-timezone-field==6.1.0 # via # -r requirements/base.txt # django-celery-beat -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/base.txt # edx-django-utils @@ -163,13 +167,13 @@ edx-completion==4.4.0 # via # -r requirements/base.txt # openedx-completion-aggregator -edx-django-utils==5.8.0 +edx-django-utils==5.9.0 # via # -r requirements/base.txt # edx-drf-extensions # edx-toggles # event-tracking -edx-drf-extensions==8.13.1 +edx-drf-extensions==9.1.2 # via # -r requirements/base.txt # edx-completion @@ -188,17 +192,17 @@ event-tracking==2.2.0 # via # -r requirements/base.txt # edx-completion -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 # via pytest factory-boy==3.3.0 # via -r requirements/test.in -faker==20.0.3 +faker==22.1.0 # via factory-boy fs==2.4.16 # via # -r requirements/base.txt # xblock -idna==3.4 +idna==3.6 # via # -r requirements/base.txt # requests @@ -212,11 +216,11 @@ jsonfield==3.1.0 # via # -r requirements/base.txt # edx-celeryutils -kombu==5.3.3 +kombu==5.3.4 # via # -r requirements/base.txt # celery -lxml==4.9.3 +lxml==5.1.0 # via # -r requirements/base.txt # xblock @@ -230,7 +234,7 @@ markupsafe==2.1.3 # jinja2 # mako # xblock -newrelic==9.1.2 +newrelic==9.4.0 # via # -r requirements/base.txt # edx-django-utils @@ -242,17 +246,17 @@ pbr==6.0.0 # via # -r requirements/base.txt # stevedore -pillow==10.1.0 +pillow==10.2.0 # via # -r requirements/base.txt # reportlab pluggy==1.3.0 # via pytest -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/base.txt # click-repl -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/base.txt # edx-django-utils @@ -275,9 +279,9 @@ pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils -pypdf==3.17.1 +pypdf==3.17.4 # via -r requirements/base.txt -pytest==7.4.3 +pytest==7.4.4 # via # pytest-cov # pytest-django @@ -314,7 +318,7 @@ pyyaml==6.0.1 # -r requirements/base.txt # code-annotations # xblock -reportlab==4.0.7 +reportlab==4.0.9 # via -r requirements/base.txt requests==2.31.0 # via @@ -362,7 +366,7 @@ tomli==2.0.1 # via # coverage # pytest -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/base.txt # asgiref @@ -370,7 +374,7 @@ typing-extensions==4.8.0 # faker # kombu # pypdf -tzdata==2023.3 +tzdata==2023.4 # via # -r requirements/base.txt # backports-zoneinfo @@ -386,7 +390,7 @@ vine==5.1.0 # amqp # celery # kombu -wcwidth==0.2.10 +wcwidth==0.2.13 # via # -r requirements/base.txt # prompt-toolkit @@ -398,7 +402,7 @@ webob==1.8.7 # via # -r requirements/base.txt # xblock -xblock==1.8.1 +xblock==1.9.1 # via # -r requirements/base.txt # edx-completion From ee44ba03d6bfe6553e41bb4850502def81569f0f Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 00:52:27 +0100 Subject: [PATCH 19/46] docs: add quickstart docs --- docs/quickstarts/images/assets.png | Bin 0 -> 39528 bytes docs/quickstarts/images/course_config.png | Bin 0 -> 46886 bytes docs/quickstarts/images/course_schedule.png | Bin 0 -> 126507 bytes docs/quickstarts/images/type_achievement.png | Bin 0 -> 149794 bytes docs/quickstarts/images/type_completion.png | Bin 0 -> 98561 bytes docs/quickstarts/index.rst | 69 +++++++++++++++++++ 6 files changed, 69 insertions(+) create mode 100644 docs/quickstarts/images/assets.png create mode 100644 docs/quickstarts/images/course_config.png create mode 100644 docs/quickstarts/images/course_schedule.png create mode 100644 docs/quickstarts/images/type_achievement.png create mode 100644 docs/quickstarts/images/type_completion.png diff --git a/docs/quickstarts/images/assets.png b/docs/quickstarts/images/assets.png new file mode 100644 index 0000000000000000000000000000000000000000..0d970c0c90eca6ab8ec7d037042a405740e54f3b GIT binary patch literal 39528 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
-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 literal 0 HcmV?d00001 diff --git a/docs/quickstarts/images/course_config.png b/docs/quickstarts/images/course_config.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8f505e908014e4ceafde982c5a450b51f1cee5 GIT binary patch literal 46886 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 literal 0 HcmV?d00001 diff --git a/docs/quickstarts/images/course_schedule.png b/docs/quickstarts/images/course_schedule.png new file mode 100644 index 0000000000000000000000000000000000000000..ec7736035cf17632f04371d580c2b2a868d26559 GIT binary patch literal 126507 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 z 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 ZDEmqH~Y6XD53#OK5(I!s!{sYHfILrV5 literal 0 HcmV?d00001 diff --git a/docs/quickstarts/images/type_achievement.png b/docs/quickstarts/images/type_achievement.png new file mode 100644 index 0000000000000000000000000000000000000000..1dbf8a94e135fa261b8cf1129f507f24f313e25a GIT binary patch literal 149794 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 RetrievalFunc [label="runs"] + RetrievalFunc -> GenerationFunc [label="sends data to"] + CertificateType -> CertificateCourseConfiguration [label="provides default options"] + GenerationFunc -> Certificate [label="generates"] + } + +Preparations +============ + +1. Go to ``Django admin -> Openedx_Certificates``. +2. Go to the ``External certificate assets`` section and add your certificate template. + You should also add all the assets that are used in the template (images, fonts, etc.). + + .. image:: ./images/assets.png + +3. Create a new certificate type in the ``External certificate types`` section. + Certificate types are reusable and can be used for multiple courses. + Example of creating a certificate type. + + a. To create a certificate of completion, use the ``retrieve_course_completions`` + retrieval function. Ignore the "Custom options" for now. Click the + "Save and continue editing" button instead. You will see the description of all + optional parameters here. + + .. image:: ./images/type_completion.png + + You can add a custom option to specify the minimum completion required to + receive the certificate. For example, if you want to issue a certificate only + to students who achieved a completion of 80% or higher, you can add a custom + option with the name ``required_completion`` and the value ``0.8``. + b. To create a certificate of achievement, use the ``retrieve_subsection_grades``. + The process is similar to the one described above. The customization options + for minimum grade are a bit more complex, so make sure to read the description + of the retrieval function. The generation function options are identical to + the ones for the certificate of completion. + + .. image:: ./images/type_achievement.png + +4. Configure the certificate type for a course in the ``External certificates course + configurations`` section. You can also specify the custom options here to override + the ones specified in the certificate type. For example, you can specify a different + minimum completion for a specific course. Or, you can use a different certificate + template for a specific course. + + .. image:: ./images/course_config.png + +5. Once you press the "Save and continue editing" button, you will see the "Generate + certificates" button. Press it to generate certificates for all students who meet + the requirements. +6. You can also create a scheduled task to generate certificates automatically. + On the course configuration page, you will see the "Associated periodic tasks" + section. Here, you can set a custom schedule for generating certificates. + + .. image:: ./images/course_schedule.png From bc2b02850e5b45cd67271054021e1cb5c3db015d Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 00:53:19 +0100 Subject: [PATCH 20/46] fix: use correct format for generation function options docstring --- openedx_certificates/generators.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index 7ced968..d606f60 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -115,10 +115,16 @@ def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: :param course_id: The ID of the course the learner completed. :param user: The user to generate the certificate for. :param certificate_uuid: The UUID of the certificate to generate. - :param options: A dictionary containing the following keys: - - template_path: The path to the PDF template file. - - output_path: The path to save the generated certificate PDF file. + :param options: The custom options for the certificate. :returns: The URL of the saved certificate. + + Options: + - template: The path to the PDF template file. + - template_two-lines: The path to the PDF template file for two-line course names. + A two-line course name is specified by using a semicolon as a separator. + - font: The name of the font to use. + - name_y: The Y coordinate of the name on the certificate (vertical position on the template). + - course_name_y: The Y coordinate of the course name on the certificate (vertical position on the template). """ log.info("Starting certificate generation for user %s", user.id) # Get template from the ExternalCertificateAsset. From 2be923be6378067ad856533acd95a361703693ce Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 01:02:42 +0100 Subject: [PATCH 21/46] feat: send emails for generated certificates --- openedx_certificates/models.py | 20 +++++++++++++++++ openedx_certificates/pipelines.py | 18 --------------- .../certificate_generated/email/body.html | 22 +++++++++++++++++++ .../certificate_generated/email/body.txt | 13 +++++++++++ .../certificate_generated/email/from_name.txt | 1 + .../certificate_generated/email/head.html | 0 .../certificate_generated/email/subject.txt | 4 ++++ tests/test_models.py | 8 +++++-- 8 files changed, 66 insertions(+), 20 deletions(-) delete mode 100644 openedx_certificates/pipelines.py create mode 100644 openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html create mode 100644 openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt create mode 100644 openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/from_name.txt create mode 100644 openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/head.html create mode 100644 openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index b358961..3bafded 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -15,9 +15,11 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django_celery_beat.models import IntervalSchedule, PeriodicTask +from edx_ace import Message, Recipient, ace from model_utils.models import TimeStampedModel from opaque_keys.edx.django.models import CourseKeyField +from openedx_certificates.compat import get_course_name from openedx_certificates.exceptions import AssetNotFoundError, CertificateGenerationError if TYPE_CHECKING: # pragma: no cover @@ -219,6 +221,8 @@ def generate_certificate_for_user(self, user_id: int, celery_task_id: int = 0): certificate.save() msg = f'Failed to generate the {certificate.uuid=} for {user_id=} with {self.id=}.' raise CertificateGenerationError(msg) from exc + else: + certificate.send_email() class ExternalCertificate(TimeStampedModel): @@ -270,6 +274,22 @@ class Meta: # noqa: D106 def __str__(self): # noqa: D105 return f"{self.certificate_type} for {self.user_full_name} in {self.course_id}" + def send_email(self): + """Send a certificate link to the student.""" + course_name = get_course_name(self.course_id) + user = get_user_model().objects.get(id=self.user_id) + msg = Message( + name="certificate_generated", + app_label="openedx_certificates", + recipient=Recipient(lms_user_id=user.id, email_address=user.email), + language='en', + context={ + 'certificate_link': self.download_url, + 'course_name': course_name, + }, + ) + ace.send(msg) + class ExternalCertificateAsset(TimeStampedModel): """ diff --git a/openedx_certificates/pipelines.py b/openedx_certificates/pipelines.py deleted file mode 100644 index 8da560d..0000000 --- a/openedx_certificates/pipelines.py +++ /dev/null @@ -1,18 +0,0 @@ -"""TODO: Add some docstring here. We may also want to move this to a different file.""" - -from django.contrib.auth.models import User -from edx_ace import Message, Recipient, ace - - -def send_email(user: User, certificate_link: str): - """Send a certificate link to the student.""" - msg = Message( - name="Certificate", - app_label="openedx_certificates", - recipient=Recipient(lms_user_id=user.id, email_address=user.email), - language='en', - context={ - 'certificate_link': certificate_link, - }, - ) - ace.send(msg) diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html new file mode 100644 index 0000000..68dc5f3 --- /dev/null +++ b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html @@ -0,0 +1,22 @@ +{% load i18n %}{% autoescape off %} + +

+ {% blocktrans %}Congratulations on successfully completing your course at {{ platform_name }}!{% endblocktrans %} +

+ +

+ {% blocktrans %}We are pleased to inform you that your certificate for the course "{{ course_name }}" is now available. This certificate acknowledges your dedication and hard work.{% endblocktrans %} +

+ +

+ {% trans "To view and download your certificate, please click on the following link:" %} +

+

{{ certificate_link }}

+ +
+
+ {% blocktrans %}Thank you for choosing {{ platform_name }} for your learning journey. We look forward to seeing you in more courses in the future.{% endblocktrans %} +
+
+ +{% endautoescape %} diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt new file mode 100644 index 0000000..388829b --- /dev/null +++ b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt @@ -0,0 +1,13 @@ +{% load i18n %}{% autoescape off %} + +{% blocktrans %}Congratulations on successfully completing your course at {{ platform_name }}!{% endblocktrans %} + +{% blocktrans %}We are pleased to inform you that your certificate for the course "{{ course_name }}" is now available. This certificate acknowledges your dedication and hard work.{% endblocktrans %} + +{% trans "To view and download your certificate, please click on the following link:" %} + +{{ certificate_link }} + +{% blocktrans %}Thank you for choosing {{ platform_name }} for your learning journey. We look forward to seeing you in more courses in the future.{% endblocktrans %} + +{% endautoescape %} diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/from_name.txt b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/from_name.txt new file mode 100644 index 0000000..dcbc23c --- /dev/null +++ b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/from_name.txt @@ -0,0 +1 @@ +{{ platform_name }} diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/head.html b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/head.html new file mode 100644 index 0000000..e69de29 diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt new file mode 100644 index 0000000..6c6d18e --- /dev/null +++ b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans trimmed %}Congratulations! Your Course Certificate from {{ platform_name }} is Ready{% endblocktrans %} +{% endautoescape %} diff --git a/tests/test_models.py b/tests/test_models.py index cf4ed4f..ebbd0f4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -157,7 +157,8 @@ def test_filter_out_user_ids_with_certificates(self): assert filtered_users == [3, 6] @pytest.mark.django_db() - def test_generate_certificate_for_user(self): + @patch.object(ExternalCertificate, 'send_email') + def test_generate_certificate_for_user(self, mock_send_email: Mock): """Test the generate_certificate_for_user method.""" user = UserFactory.create() task_id = 123 @@ -172,9 +173,11 @@ def test_generate_certificate_for_user(self): generation_task_id=task_id, download_url="test_url", ).exists() + mock_send_email.assert_called_once() @pytest.mark.django_db() - def test_generate_certificate_for_user_update_existing(self): + @patch.object(ExternalCertificate, 'send_email') + def test_generate_certificate_for_user_update_existing(self, mock_send_email: Mock): """Test the generate_certificate_for_user method updates an existing certificate.""" user = UserFactory.create() @@ -198,6 +201,7 @@ def test_generate_certificate_for_user_update_existing(self): generation_task_id=0, download_url="test_url", ).exists() + mock_send_email.assert_called_once() @pytest.mark.django_db() @patch('openedx_certificates.models.import_module') From 5115f4082b3671b5b3fa4320ff71e55861bb3a8b Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 01:46:05 +0100 Subject: [PATCH 22/46] fixup! feat: send emails for generated certificates --- openedx_certificates/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index 3bafded..fbcad74 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -286,6 +286,7 @@ def send_email(self): context={ 'certificate_link': self.download_url, 'course_name': course_name, + 'platform_name': settings.PLATFORM_NAME, }, ) ace.send(msg) From 263aa51038317b45f3b057698b58b4b3bc78cc90 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 02:56:58 +0100 Subject: [PATCH 23/46] fix: fix celery tasks --- openedx_certificates/models.py | 10 +++++++--- openedx_certificates/tasks.py | 4 ++-- tests/test_models.py | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index fbcad74..ac70e0e 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -99,19 +99,23 @@ def __str__(self): # noqa: D105 def save(self, *args, **kwargs): """Create a new PeriodicTask every time a new ExternalCertificateCourseConfiguration is created.""" - if self._state.adding: - from openedx_certificates.tasks import generate_certificates_for_course_task # Avoid circular imports. + from openedx_certificates.tasks import generate_certificates_for_course_task as task # Avoid circular imports. + + task_path = f"{task.__module__}.{task.__name__}" + if self._state.adding: schedule, created = IntervalSchedule.objects.get_or_create(every=10, period=IntervalSchedule.DAYS) self.periodic_task = PeriodicTask.objects.create( enabled=False, interval=schedule, name=f'{self.certificate_type} in {self.course_id}', - task=generate_certificates_for_course_task, + task=task_path, ) super().save(*args, **kwargs) + # Update the task on each save to prevent it from getting out of sync (e.g., after changing a task definition). + self.periodic_task.task = task_path # Update the args of the PeriodicTask to include the ID of the ExternalCertificateCourseConfiguration. self.periodic_task.args = json.dumps([self.id]) self.periodic_task.save() diff --git a/openedx_certificates/tasks.py b/openedx_certificates/tasks.py index 989b7cb..45b839e 100644 --- a/openedx_certificates/tasks.py +++ b/openedx_certificates/tasks.py @@ -37,8 +37,8 @@ def generate_certificates_for_course_task(course_config_id: int): """ course_config = ExternalCertificateCourseConfiguration.objects.get(id=course_config_id) user_ids = course_config.get_eligible_user_ids() - for name in user_ids: - generate_certificate_for_user_task.apply_async(course_config_id, name) + for user_id in user_ids: + generate_certificate_for_user_task.delay(course_config_id, user_id) @app.task diff --git a/tests/test_models.py b/tests/test_models.py index ebbd0f4..c2bf3a2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -100,6 +100,7 @@ def test_periodic_task_is_auto_created(self): assert periodic_task.enabled is False assert periodic_task.name == str(self.course_config) assert periodic_task.args == f'[{self.course_config.id}]' + assert periodic_task.task == 'celery.local.generate_certificates_for_course_task' def test_str_representation(self): """Test the string representation of the model.""" From aeeaa724209c5840eda147ea9ea3236d3ca9bf27 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 18:32:18 +0100 Subject: [PATCH 24/46] feat: do not regenerate certificates all the time --- openedx_certificates/models.py | 2 -- tests/test_models.py | 1 - 2 files changed, 3 deletions(-) diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index ac70e0e..3e1db70 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -148,8 +148,6 @@ def filter_out_user_ids_with_certificates(self, user_ids: list[int]) -> list[int 1. Do not have a certificate for this course and certificate type. 2. Have such a certificate with an error status. """ - # TODO: Delete this after testing. - return user_ids users_ids_with_certificates = ExternalCertificate.objects.filter( models.Q(course_id=self.course_id), models.Q(certificate_type=self.certificate_type), diff --git a/tests/test_models.py b/tests/test_models.py index c2bf3a2..6cc5ca4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -111,7 +111,6 @@ def test_get_eligible_user_ids(self): eligible_user_ids = self.course_config.get_eligible_user_ids() assert eligible_user_ids == [1, 2, 3] - @pytest.mark.xfail(reason="The filtering is currently disabled for testing purposes.") @pytest.mark.django_db() def test_filter_out_user_ids_with_certificates(self): """Test the filter_out_user_ids_with_certificates method.""" From b28e02220b5d47762302ef03ef0d06f4423f1d89 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 18:34:34 +0100 Subject: [PATCH 25/46] feat: do not send emails to inactive users --- openedx_certificates/models.py | 5 ++++- tests/test_models.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index 3e1db70..89692cf 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -224,7 +224,10 @@ def generate_certificate_for_user(self, user_id: int, celery_task_id: int = 0): msg = f'Failed to generate the {certificate.uuid=} for {user_id=} with {self.id=}.' raise CertificateGenerationError(msg) from exc else: - certificate.send_email() + # TODO: In the future, we want to check this before generating the certificate. + # Perhaps we could even include this in a processor to optimize it. + if user.is_active and user.has_usable_password(): + certificate.send_email() class ExternalCertificate(TimeStampedModel): diff --git a/tests/test_models.py b/tests/test_models.py index 6cc5ca4..6a4414d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -175,6 +175,23 @@ def test_generate_certificate_for_user(self, mock_send_email: Mock): ).exists() mock_send_email.assert_called_once() + # For now, we only prevent the generation task from sending emails to inactive users. + # In the future, we may want to prevent the generation task from generating certificates for inactive users. + + user = UserFactory.create(is_active=False) + + self.course_config.generate_certificate_for_user(user.id, task_id) + assert ExternalCertificate.objects.filter(course_id=self.course_config.course_id).count() == 2 + mock_send_email.assert_called_once() + + user = UserFactory.create() + user.set_unusable_password() + user.save() + + self.course_config.generate_certificate_for_user(user.id, task_id) + assert ExternalCertificate.objects.filter(course_id=self.course_config.course_id).count() == 3 + mock_send_email.assert_called_once() + @pytest.mark.django_db() @patch.object(ExternalCertificate, 'send_email') def test_generate_certificate_for_user_update_existing(self, mock_send_email: Mock): From d5d99f6596be96500362d1272fc76a1a2b8a1119 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 19:28:52 +0100 Subject: [PATCH 26/46] fix: do not regenerate certificates from the celery task --- openedx_certificates/tasks.py | 9 +++- tests/test_tasks.py | 79 +++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/test_tasks.py diff --git a/openedx_certificates/tasks.py b/openedx_certificates/tasks.py index 45b839e..e3a5565 100644 --- a/openedx_certificates/tasks.py +++ b/openedx_certificates/tasks.py @@ -2,10 +2,13 @@ from __future__ import annotations +import logging + from openedx_certificates.compat import get_celery_app from openedx_certificates.models import ExternalCertificateCourseConfiguration app = get_celery_app() +log = logging.getLogger(__name__) @app.task @@ -37,7 +40,11 @@ def generate_certificates_for_course_task(course_config_id: int): """ course_config = ExternalCertificateCourseConfiguration.objects.get(id=course_config_id) user_ids = course_config.get_eligible_user_ids() - for user_id in user_ids: + log.info("The following users are eligible in %s: %s", course_config.course_id, user_ids) + filtered_user_ids = course_config.filter_out_user_ids_with_certificates(user_ids) + log.info("The filtered users eligible in %s: %s", course_config.course_id, filtered_user_ids) + + for user_id in filtered_user_ids: generate_certificate_for_user_task.delay(course_config_id, user_id) diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 0000000..2887690 --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,79 @@ +"""Tests for the openedx-certificates Celery tasks.""" + +from unittest.mock import MagicMock, Mock, PropertyMock, patch + +import pytest + +from openedx_certificates.tasks import ( + generate_all_certificates_task, + generate_certificate_for_user_task, + generate_certificates_for_course_task, +) + + +@pytest.mark.django_db() +def test_generate_certificate_for_user(): + """Test if the `generate_certificate_for_user` method is called with correct parameters.""" + course_config_id = 123 + user_id = 456 + task_id = 789 + + with patch('openedx_certificates.models.ExternalCertificateCourseConfiguration.objects.get') as mock_get, patch( + 'openedx_certificates.tasks.generate_certificate_for_user_task' + ) as mock_task: + mock_config = Mock() + mock_get.return_value = mock_config + + mock_request = Mock() + type(mock_request).id = PropertyMock(return_value=task_id) + type(mock_task).request = PropertyMock(return_value=mock_request) + + # Call the actual task + generate_certificate_for_user_task(course_config_id, user_id) + + mock_config.generate_certificate_for_user.assert_called_once_with(user_id, task_id) + + +@pytest.mark.django_db() +def test_generate_certificates_for_course_with_filtering(): + """Test if `generate_certificate_for_user_task.delay` is called for each filtered eligible user.""" + course_config_id = 123 + all_eligible_user_ids = [1, 2, 3, 4] # Initial set of eligible user IDs + filtered_user_ids = [1, 3] # User IDs after filtering (e.g., users 2 and 4 already have certificates) + + with patch('openedx_certificates.models.ExternalCertificateCourseConfiguration.objects.get') as mock_get, patch( + 'openedx_certificates.tasks.generate_certificate_for_user_task.delay' + ) as mock_delay: + mock_config = Mock() + mock_get.return_value = mock_config + + # Mocking the methods to return predefined lists + mock_config.get_eligible_user_ids.return_value = all_eligible_user_ids + mock_config.filter_out_user_ids_with_certificates.return_value = filtered_user_ids + + generate_certificates_for_course_task(course_config_id) + + # Ensure that the delay method is called only for filtered user IDs + assert mock_delay.call_count == len(filtered_user_ids) + for user_id in filtered_user_ids: + mock_delay.assert_any_call(course_config_id, user_id) + + +@pytest.mark.django_db() +def test_generate_all_certificates(): + """Test if `generate_certificates_for_course_task.delay` is called for each enabled configuration.""" + config_ids = [101, 102, 103] + + # Create a mock QuerySet + mock_queryset = MagicMock() + mock_queryset.values_list.return_value = config_ids + + with patch( + 'openedx_certificates.models.ExternalCertificateCourseConfiguration.get_enabled_configurations', + return_value=mock_queryset, + ), patch('openedx_certificates.tasks.generate_certificates_for_course_task.delay') as mock_delay: + generate_all_certificates_task() + + assert mock_delay.call_count == len(config_ids) + for config_id in config_ids: + mock_delay.assert_any_call(config_id) From 033a49a82805cbcdae8be3148b731e6d1ce7ff07 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 19:31:45 +0100 Subject: [PATCH 27/46] fixup! fix: do not regenerate certificates from the celery task --- tests/test_tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 2887690..439f4ac 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -19,7 +19,7 @@ def test_generate_certificate_for_user(): task_id = 789 with patch('openedx_certificates.models.ExternalCertificateCourseConfiguration.objects.get') as mock_get, patch( - 'openedx_certificates.tasks.generate_certificate_for_user_task' + 'openedx_certificates.tasks.generate_certificate_for_user_task', ) as mock_task: mock_config = Mock() mock_get.return_value = mock_config @@ -42,7 +42,7 @@ def test_generate_certificates_for_course_with_filtering(): filtered_user_ids = [1, 3] # User IDs after filtering (e.g., users 2 and 4 already have certificates) with patch('openedx_certificates.models.ExternalCertificateCourseConfiguration.objects.get') as mock_get, patch( - 'openedx_certificates.tasks.generate_certificate_for_user_task.delay' + 'openedx_certificates.tasks.generate_certificate_for_user_task.delay', ) as mock_delay: mock_config = Mock() mock_get.return_value = mock_config From 7daf6f3dcb336716d2d349cb48b3765732e27f2c Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 11 Jan 2024 19:38:31 +0100 Subject: [PATCH 28/46] feat: run Django admin generation tasks with Celery --- openedx_certificates/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py index b9b355d..8a6138b 100644 --- a/openedx_certificates/admin.py +++ b/openedx_certificates/admin.py @@ -18,6 +18,7 @@ ExternalCertificateCourseConfiguration, ExternalCertificateType, ) +from .tasks import generate_certificates_for_course_task if TYPE_CHECKING: # pragma: no cover from django.http import HttpRequest @@ -170,8 +171,7 @@ def generate_certificates(self, _request: HttpRequest, obj: ExternalCertificateC _request: The request object. obj: The ExternalCertificateCourse instance. """ - # TODO: Use the celery task instead of the generate_certificates method. - obj.generate_certificates() + generate_certificates_for_course_task.delay(obj.id) change_actions = ('generate_certificates',) From 4a225d80dc40b836cfff44b5f1958989c886973e Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 15 Jan 2024 21:40:21 +0100 Subject: [PATCH 29/46] fixup! fix: fix celery tasks --- openedx_certificates/models.py | 3 ++- tests/test_models.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index 89692cf..b03cf69 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -101,7 +101,8 @@ def save(self, *args, **kwargs): """Create a new PeriodicTask every time a new ExternalCertificateCourseConfiguration is created.""" from openedx_certificates.tasks import generate_certificates_for_course_task as task # Avoid circular imports. - task_path = f"{task.__module__}.{task.__name__}" + # Use __wrapped__ to get the original function, as the task is wrapped by the @app.task decorator. + task_path = f"{task.__wrapped__.__module__}.{task.__wrapped__.__name__}" if self._state.adding: schedule, created = IntervalSchedule.objects.get_or_create(every=10, period=IntervalSchedule.DAYS) diff --git a/tests/test_models.py b/tests/test_models.py index 6a4414d..ed4e9ea 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -100,7 +100,7 @@ def test_periodic_task_is_auto_created(self): assert periodic_task.enabled is False assert periodic_task.name == str(self.course_config) assert periodic_task.args == f'[{self.course_config.id}]' - assert periodic_task.task == 'celery.local.generate_certificates_for_course_task' + assert periodic_task.task == 'openedx_certificates.tasks.generate_certificates_for_course_task' def test_str_representation(self): """Test the string representation of the model.""" From 07335fb4bed8b0751080ceebc2fe995f87e01837 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 15 Jan 2024 22:11:05 +0100 Subject: [PATCH 30/46] fix: validate course ID in Django admin instead of raising an error --- openedx_certificates/admin.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py index 8a6138b..c6a8e8d 100644 --- a/openedx_certificates/admin.py +++ b/openedx_certificates/admin.py @@ -8,9 +8,12 @@ from django import forms from django.contrib import admin +from django.core.exceptions import ValidationError from django.utils.html import format_html from django_object_actions import DjangoObjectActions, action from django_reverse_admin import ReverseModelAdmin +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey from .models import ( ExternalCertificate, @@ -108,6 +111,22 @@ class ExternalCertificateAssetAdmin(admin.ModelAdmin): # noqa: D101 prepopulated_fields = {"asset_slug": ("description",)} # noqa: RUF012 +class ExternalCertificateCourseConfigurationForm(forms.ModelForm): # noqa: D101 + class Meta: # noqa: D106 + model = ExternalCertificateCourseConfiguration + fields = ('course_id', 'certificate_type', 'custom_options') + + def clean_course_id(self) -> CourseKey: + """Validate the course_id field.""" + course_id = self.cleaned_data.get('course_id') + try: + CourseKey.from_string(course_id) + except InvalidKeyError as exc: + msg = "Invalid course ID format. The correct format is 'course-v1:{Organization}+{Course}+{Run}'." + raise ValidationError(msg) from exc + return course_id + + @admin.register(ExternalCertificateCourseConfiguration) class ExternalCertificateCourseConfigurationAdmin(DjangoObjectActions, ReverseModelAdmin): """ @@ -117,6 +136,7 @@ class ExternalCertificateCourseConfigurationAdmin(DjangoObjectActions, ReverseMo The reverse inline provides a way to manage the periodic task from the configuration page. """ + form = ExternalCertificateCourseConfigurationForm inline_type = 'stacked' inline_reverse = [ # noqa: RUF012 ( From 711dbf61564dcd4d2c4b35dfd8dbe3ec1f2ad6fc Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 15 Jan 2024 22:20:03 +0100 Subject: [PATCH 31/46] temp: add certificate created and modified date to Django admin --- openedx_certificates/admin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py index c6a8e8d..1746ffa 100644 --- a/openedx_certificates/admin.py +++ b/openedx_certificates/admin.py @@ -198,9 +198,20 @@ def generate_certificates(self, _request: HttpRequest, obj: ExternalCertificateC @admin.register(ExternalCertificate) class ExternalCertificateAdmin(admin.ModelAdmin): # noqa: D101 - list_display = ('user_id', 'user_full_name', 'course_id', 'certificate_type', 'status', 'url') + list_display = ( + 'user_id', + 'user_full_name', + 'course_id', + 'certificate_type', + 'status', + 'url', + 'created', + 'modified', + ) readonly_fields = ( 'user_id', + 'created', + 'modified', 'user_full_name', 'course_id', 'certificate_type', From bfcc991985580e213f71fe2898d6643b2d9fa167 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 15 Jan 2024 23:11:20 +0100 Subject: [PATCH 32/46] feat: allow changing text color on generated PDF --- openedx_certificates/generators.py | 30 ++++++++++++++++++++++++++++-- tests/test_generators.py | 23 +++++++++++++++++++---- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index d606f60..1c66f8f 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -64,18 +64,42 @@ def _write_text_on_template(template: any, font: str, username: str, course_name :param font: Font name. :param username: The name of the user to generate the certificate for. :param course_name: The name of the course the learner completed. - :param options: A dictionary containing the Y coordinates for name and course name. + :param options: A dictionary documented in the `generate_pdf_certificate` function. :returns: A canvas with written data. """ + + def hex_to_rgb(hex_color: str) -> tuple[float, float, float]: + """ + Convert a hexadecimal color code to an RGB tuple with floating-point values. + + :param hex_color: A hexadecimal color string, which can start with '#' and be either 3 or 6 characters long. + :returns: A tuple representing the RGB color as (red, green, blue), with each value ranging from 0.0 to 1.0. + """ + hex_color = hex_color.lstrip('#') + # Expand shorthand form (e.g. "158" to "115588") + if len(hex_color) == 3: + hex_color = ''.join([c * 2 for c in hex_color]) + + # noinspection PyTypeChecker + return tuple(int(hex_color[i : i + 2], 16) / 255 for i in range(0, 6, 2)) + template_width, template_height = template.mediabox[2:] pdf_canvas = canvas.Canvas(io.BytesIO(), pagesize=(template_width, template_height)) - pdf_canvas.setFont(font, 32) + # Write the learner name. + pdf_canvas.setFont(font, 32) + name_color = options.get('name_color', '#000') + pdf_canvas.setFillColorRGB(*hex_to_rgb(name_color)) + name_x = (template_width - pdf_canvas.stringWidth(username)) / 2 name_y = options.get('name_y', 290) pdf_canvas.drawString(name_x, name_y, username) + # Write the course name. pdf_canvas.setFont(font, 28) + course_name_color = options.get('course_name_color', '#000') + pdf_canvas.setFillColorRGB(*hex_to_rgb(course_name_color)) + course_name_x = (template_width - pdf_canvas.stringWidth(course_name)) / 2 course_name_y = options.get('course_name_y', 220) pdf_canvas.drawString(course_name_x, course_name_y, course_name) @@ -124,7 +148,9 @@ def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: A two-line course name is specified by using a semicolon as a separator. - font: The name of the font to use. - name_y: The Y coordinate of the name on the certificate (vertical position on the template). + - name_color: The color of the name on the certificate (hexadecimal color code). - course_name_y: The Y coordinate of the course name on the certificate (vertical position on the template). + - course_name_color: The color of the course name on the certificate (hexadecimal color code). """ log.info("Starting certificate generation for user %s", user.id) # Get template from the ExternalCertificateAsset. diff --git a/tests/test_generators.py b/tests/test_generators.py index 5301b47..b78b411 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -61,14 +61,27 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas @pytest.mark.parametrize( - ("username", "course_name", "options"), + ("username", "course_name", "options", "expected_name_color", "expected_course_name_color"), [ - ('John Doe', 'Programming 101', {}), # No options - use default coordinates. - ('John Doe', 'Programming 101', {'name_y': 250, 'course_name_y': 200}), # Custom coordinates. + ('John Doe', 'Programming 101', {}, (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), # No options - use default coordinates. + ( + 'John Doe', + 'Programming 101', + {'name_y': 250, 'course_name_y': 200, 'name_color': '123', 'course_name_color': '#9B192A'}, + (17 / 255, 34 / 255, 51 / 255), + (155 / 255, 25 / 255, 42 / 255), + ), # Custom coordinates and colors. ], ) @patch('openedx_certificates.generators.canvas.Canvas', return_value=Mock(stringWidth=Mock(return_value=10))) -def test_write_text_on_template(mock_canvas_class: Mock, username: str, course_name: str, options: dict[str, int]): +def test_write_text_on_template( # noqa: PLR0913 + mock_canvas_class: Mock, + username: str, + course_name: str, + options: dict[str, int], + expected_name_color: tuple[float, float, float], + expected_course_name_color: tuple[float, float, float], +): """Test the _write_text_on_template function.""" template_height = 300 template_width = 200 @@ -99,9 +112,11 @@ def test_write_text_on_template(mock_canvas_class: Mock, username: str, course_n # Check the calls to setFont and drawString methods on Canvas object assert canvas_object.setFont.call_args_list[0] == call(font, 32) + assert canvas_object.setFillColorRGB.call_args_list[0] == call(*expected_name_color) assert canvas_object.drawString.call_args_list[0] == call(expected_name_x, expected_name_y, username) assert canvas_object.setFont.call_args_list[1] == call(font, 28) + assert canvas_object.setFillColorRGB.call_args_list[1] == call(*expected_course_name_color) assert canvas_object.drawString.call_args_list[1] == call( expected_course_name_x, expected_course_name_y, From 2de93b2e9eb0564ead1218ed16e9d3fe4a0983d5 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 16 Jan 2024 01:13:56 +0100 Subject: [PATCH 33/46] feat: add issue date to PDF certificates --- openedx_certificates/compat.py | 11 +++++++ openedx_certificates/generators.py | 15 ++++++++- tests/test_generators.py | 50 ++++++++++++++++++++---------- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/openedx_certificates/compat.py b/openedx_certificates/compat.py index b367844..58c5620 100644 --- a/openedx_certificates/compat.py +++ b/openedx_certificates/compat.py @@ -7,8 +7,10 @@ from __future__ import annotations from contextlib import contextmanager +from datetime import datetime from typing import TYPE_CHECKING +import pytz from celery import Celery from django.conf import settings @@ -74,3 +76,12 @@ def get_course_grade_factory(): # noqa: ANN201 from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory return CourseGradeFactory() + + +def get_localized_certificate_date() -> str: + """Get the localized date from Open edX.""" + # noinspection PyUnresolvedReferences,PyPackageRequirements + from common.djangoapps.util.date_utils import strftime_localized + + date = datetime.now(pytz.timezone(settings.TIME_ZONE)) + return strftime_localized(date, settings.CERTIFICATE_DATE_FORMAT) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index 1c66f8f..e3ddd0a 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -21,7 +21,7 @@ from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas -from openedx_certificates.compat import get_course_name +from openedx_certificates.compat import get_course_name, get_localized_certificate_date from openedx_certificates.models import ExternalCertificateAsset log = logging.getLogger(__name__) @@ -103,6 +103,17 @@ def hex_to_rgb(hex_color: str) -> tuple[float, float, float]: course_name_x = (template_width - pdf_canvas.stringWidth(course_name)) / 2 course_name_y = options.get('course_name_y', 220) pdf_canvas.drawString(course_name_x, course_name_y, course_name) + + # Write the issue date. + issue_date = get_localized_certificate_date() + pdf_canvas.setFont(font, 20) + issue_date_color = options.get('issue_date_color', '#000') + pdf_canvas.setFillColorRGB(*hex_to_rgb(issue_date_color)) + + issue_date_x = (template_width - pdf_canvas.stringWidth(issue_date)) / 2 + issue_date_y = options.get('issue_date_y', 120) + pdf_canvas.drawString(issue_date_x, issue_date_y, issue_date) + return pdf_canvas @@ -151,6 +162,8 @@ def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: - name_color: The color of the name on the certificate (hexadecimal color code). - course_name_y: The Y coordinate of the course name on the certificate (vertical position on the template). - course_name_color: The color of the course name on the certificate (hexadecimal color code). + - issue_date_y: The Y coordinate of the issue date on the certificate (vertical position on the template). + - issue_date_color: The color of the issue date on the certificate (hexadecimal color code). """ log.info("Starting certificate generation for user %s", user.id) # Get template from the ExternalCertificateAsset. diff --git a/tests/test_generators.py b/tests/test_generators.py index b78b411..fd4d6a0 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -61,32 +61,36 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas @pytest.mark.parametrize( - ("username", "course_name", "options", "expected_name_color", "expected_course_name_color"), + ("options", "expected"), [ - ('John Doe', 'Programming 101', {}, (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), # No options - use default coordinates. + ({}, {}), # No options - use default coordinates and colors. ( - 'John Doe', - 'Programming 101', - {'name_y': 250, 'course_name_y': 200, 'name_color': '123', 'course_name_color': '#9B192A'}, - (17 / 255, 34 / 255, 51 / 255), - (155 / 255, 25 / 255, 42 / 255), + { + 'name_y': 250, + 'course_name_y': 200, + 'issue_date_y': 150, + 'name_color': '123', + 'course_name_color': '#9B192A', + 'issue_date_color': '#f59a8e', + }, + { + 'name_color': (17 / 255, 34 / 255, 51 / 255), + 'course_name_color': (155 / 255, 25 / 255, 42 / 255), + 'issue_date_color': (245 / 255, 154 / 255, 142 / 255), + }, ), # Custom coordinates and colors. ], ) @patch('openedx_certificates.generators.canvas.Canvas', return_value=Mock(stringWidth=Mock(return_value=10))) -def test_write_text_on_template( # noqa: PLR0913 - mock_canvas_class: Mock, - username: str, - course_name: str, - options: dict[str, int], - expected_name_color: tuple[float, float, float], - expected_course_name_color: tuple[float, float, float], -): +def test_write_text_on_template(mock_canvas_class: Mock, options: dict[str, int], expected: dict): """Test the _write_text_on_template function.""" + username = 'John Doe' + course_name = 'Programming 101' template_height = 300 template_width = 200 font = 'Helvetica' string_width = mock_canvas_class.return_value.stringWidth.return_value + test_date = 'April 1, 2021' # Reset the mock to discard calls list from previous tests mock_canvas_class.reset_mock() @@ -95,7 +99,8 @@ def test_write_text_on_template( # noqa: PLR0913 template_mock.mediabox = [0, 0, template_width, template_height] # Call the function with test parameters and mocks - _write_text_on_template(template_mock, font, username, course_name, options) + with patch('openedx_certificates.generators.get_localized_certificate_date', return_value=test_date): + _write_text_on_template(template_mock, font, username, course_name, options) # Verifying that Canvas was the correct pagesize. # Use `call_args_list` to ignore the first argument, which is an instance of io.BytesIO. @@ -109,8 +114,15 @@ def test_write_text_on_template( # noqa: PLR0913 expected_name_y = options.get('name_y', 290) expected_course_name_x = (template_width - string_width) / 2 expected_course_name_y = options.get('course_name_y', 220) + expected_issue_date_x = (template_width - string_width) / 2 + expected_issue_date_y = options.get('issue_date_y', 120) - # Check the calls to setFont and drawString methods on Canvas object + # Expected colors for setFillColorRGB method + expected_name_color = expected.get('name_color', (0, 0, 0)) + expected_course_name_color = expected.get('course_name_color', (0, 0, 0)) + expected_issue_date_color = expected.get('issue_date_color', (0, 0, 0)) + + # Check the calls to setFont, setFillColorRGB and drawString methods on Canvas object assert canvas_object.setFont.call_args_list[0] == call(font, 32) assert canvas_object.setFillColorRGB.call_args_list[0] == call(*expected_name_color) assert canvas_object.drawString.call_args_list[0] == call(expected_name_x, expected_name_y, username) @@ -123,6 +135,10 @@ def test_write_text_on_template( # noqa: PLR0913 course_name, ) + assert canvas_object.setFont.call_args_list[2] == call(font, 20) + assert canvas_object.setFillColorRGB.call_args_list[2] == call(*expected_issue_date_color) + assert canvas_object.drawString.call_args_list[2] == call(expected_issue_date_x, expected_issue_date_y, test_date) + @override_settings(LMS_ROOT_URL="http://example.com", MEDIA_URL="media/") @pytest.mark.parametrize( From de3b56e9280e6a1cc8bf6100c7556b9644405fbb Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 16 Jan 2024 19:29:04 +0100 Subject: [PATCH 34/46] fixup! docs: add quickstart docs --- docs/quickstarts/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quickstarts/index.rst b/docs/quickstarts/index.rst index ee3453e..4ab5d8f 100644 --- a/docs/quickstarts/index.rst +++ b/docs/quickstarts/index.rst @@ -10,8 +10,8 @@ See the following diagram for a quick overview of the certificate generation pro digraph G { CertificateType [shape=box, color="black", label="Certificate Type\n\nProvides reusable configuration by storing the:\n- retrieval function\n- generation function\n- custom options"] - CertificateCourseConfiguration [shape=box, color="black", label="Certificate Course Configuration\n\n1. Stores option overrdes.\n2.Defines custom schedules for certificate generations."] - RetrievalFunc [shape=ellipse, color="blue", label="retrieval_func\n\nA function that retrieves information\n about learners eliglble for the certificate.\nIt defines the criteria for getting a certificate."] + CertificateCourseConfiguration [shape=box, color="black", label="Certificate Course Configuration\n\n1. Stores option overrides.\n2.Defines custom schedules for certificate generations."] + RetrievalFunc [shape=ellipse, color="blue", label="retrieval_func\n\nA function that retrieves information\n about learners eligible for the certificate.\nIt defines the criteria for getting a certificate."] GenerationFunc [shape=ellipse, color="blue", label="generation_func\n\nA function that defines how the certificate\ngeneration process looks like\n(e.g., it creates a PDF file)."] Certificate [shape=box, color="black", label="Certificate\n\nThe generated certificate."] From 12626cf250a3ea43f8ed2c7b1b2124d39088aec8 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 16 Jan 2024 19:29:42 +0100 Subject: [PATCH 35/46] fixup! feat: send emails for generated certificates --- .../edx_ace/certificate_generated/email/body.html | 6 +++--- .../edx_ace/certificate_generated/email/body.txt | 4 ++-- .../edx_ace/certificate_generated/email/subject.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html index 68dc5f3..03919fa 100644 --- a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html +++ b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.html @@ -1,17 +1,17 @@ {% load i18n %}{% autoescape off %}

- {% blocktrans %}Congratulations on successfully completing your course at {{ platform_name }}!{% endblocktrans %} + {% blocktrans %}Thank you for your participation in {{ course_name }} at {{ platform_name }}!{% endblocktrans %}

- {% blocktrans %}We are pleased to inform you that your certificate for the course "{{ course_name }}" is now available. This certificate acknowledges your dedication and hard work.{% endblocktrans %} + {% blocktrans %}We are happy to inform you that you have earned a certificate. You should feel very proud of the work you have done in this course. We congratulate you on your efforts and your learning.{% endblocktrans %}

{% trans "To view and download your certificate, please click on the following link:" %}

-

{{ certificate_link }}

+

View and download your certificate

diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt index 388829b..8332a4b 100644 --- a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt +++ b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/body.txt @@ -1,8 +1,8 @@ {% load i18n %}{% autoescape off %} -{% blocktrans %}Congratulations on successfully completing your course at {{ platform_name }}!{% endblocktrans %} +{% blocktrans %}Thank you for your participation in {{ course_name }} at {{ platform_name }}!{% endblocktrans %} -{% blocktrans %}We are pleased to inform you that your certificate for the course "{{ course_name }}" is now available. This certificate acknowledges your dedication and hard work.{% endblocktrans %} +{% blocktrans %}We are happy to inform you that you have earned a certificate. You should feel very proud of the work you have done in this course. We congratulate you on your efforts and your learning.{% endblocktrans %} {% trans "To view and download your certificate, please click on the following link:" %} diff --git a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt index 6c6d18e..66b5074 100644 --- a/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt +++ b/openedx_certificates/templates/openedx_certificates/edx_ace/certificate_generated/email/subject.txt @@ -1,4 +1,4 @@ {% load i18n %} {% autoescape off %} -{% blocktrans trimmed %}Congratulations! Your Course Certificate from {{ platform_name }} is Ready{% endblocktrans %} +{% blocktrans trimmed %}{{ course_name }} - Certificate{% endblocktrans %} {% endautoescape %} From de9b5a8987935d7ee312a33602bfd5d33d8a8281 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 16 Jan 2024 19:50:04 +0100 Subject: [PATCH 36/46] feat: custom domains for certificates --- openedx_certificates/generators.py | 4 ++++ tests/test_generators.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index e3ddd0a..78227a8 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -140,6 +140,10 @@ def _save_certificate(certificate: PdfWriter, certificate_uuid: UUID) -> str: url = f"{settings.LMS_ROOT_URL}{settings.MEDIA_URL}{output_path}" else: url = default_storage.url(output_path) + + if custom_domain := getattr(settings, 'CERTIFICATES_CUSTOM_DOMAIN', None): + url = f"{custom_domain}/{certificate_uuid}.pdf" + return url diff --git a/tests/test_generators.py b/tests/test_generators.py index fd4d6a0..4f1ba09 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -140,7 +140,7 @@ def test_write_text_on_template(mock_canvas_class: Mock, options: dict[str, int] assert canvas_object.drawString.call_args_list[2] == call(expected_issue_date_x, expected_issue_date_y, test_date) -@override_settings(LMS_ROOT_URL="http://example.com", MEDIA_URL="media/") +@override_settings(LMS_ROOT_URL="https://example.com", MEDIA_URL="media/") @pytest.mark.parametrize( "storage", [ @@ -181,6 +181,11 @@ def test_save_certificate(mock_contentfile: Mock, storage: DefaultStorage | Mock else: assert url == f'/{output_path}' + # Allow specifying a custom domain for certificates. + with override_settings(CERTIFICATES_CUSTOM_DOMAIN='https://example2.com'): + url = _save_certificate(certificate, certificate_uuid) + assert url == f'https://example2.com/{certificate_uuid}.pdf' + @pytest.mark.parametrize( ("course_name", "options", "expected_template_slug"), From 2a88fd8ecf3ed48fe82abdf877f11dc264235352 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 16 Jan 2024 21:22:23 +0100 Subject: [PATCH 37/46] fixup! feat: add issue date to PDF certificates --- openedx_certificates/generators.py | 2 +- tests/test_generators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index 78227a8..df731f1 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -106,7 +106,7 @@ def hex_to_rgb(hex_color: str) -> tuple[float, float, float]: # Write the issue date. issue_date = get_localized_certificate_date() - pdf_canvas.setFont(font, 20) + pdf_canvas.setFont(font, 12) issue_date_color = options.get('issue_date_color', '#000') pdf_canvas.setFillColorRGB(*hex_to_rgb(issue_date_color)) diff --git a/tests/test_generators.py b/tests/test_generators.py index 4f1ba09..f5bf3d7 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -135,7 +135,7 @@ def test_write_text_on_template(mock_canvas_class: Mock, options: dict[str, int] course_name, ) - assert canvas_object.setFont.call_args_list[2] == call(font, 20) + assert canvas_object.setFont.call_args_list[2] == call(font, 12) assert canvas_object.setFillColorRGB.call_args_list[2] == call(*expected_issue_date_color) assert canvas_object.drawString.call_args_list[2] == call(expected_issue_date_x, expected_issue_date_y, test_date) From cc3230ab49eeaf33b74f91389171fc33d4a09c25 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 16 Jan 2024 21:25:32 +0100 Subject: [PATCH 38/46] feat: display custom options for course configuration in Django admin --- openedx_certificates/admin.py | 66 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py index 1746ffa..1ad9523 100644 --- a/openedx_certificates/admin.py +++ b/openedx_certificates/admin.py @@ -28,29 +28,8 @@ from django_celery_beat.models import IntervalSchedule -class ExternalCertificateTypeAdminForm(forms.ModelForm): - """Generate a list of available functions for the function fields.""" - - retrieval_func = forms.ChoiceField(choices=[]) - generation_func = forms.ChoiceField(choices=[]) - - @staticmethod - def _available_functions(module: str, prefix: str) -> Generator[tuple[str, str], None, None]: - """ - Import a module and return all functions in it that start with a specific prefix. - - :param module: The name of the module to import. - :param prefix: The prefix of the function names to return. - - :return: A tuple containing the functions that start with the prefix in the module. - """ - # TODO: Implement plugin support for the functions. - _module = importlib.import_module(module) - return ( - (f'{obj.__module__}.{name}', f'{obj.__module__}.{name}') - for name, obj in inspect.getmembers(_module, inspect.isfunction) - if name.startswith(prefix) - ) +class DocstringOptionsMixin: + """A mixin to add the docstring of the function to the help text of the function field.""" @staticmethod def _get_docstring_custom_options(func: str) -> str: @@ -78,6 +57,31 @@ def _get_docstring_custom_options(func: str) -> str: # Use pre to preserve the newlines and indentation. return f'
{docstring}
' + +class ExternalCertificateTypeAdminForm(forms.ModelForm, DocstringOptionsMixin): + """Generate a list of available functions for the function fields.""" + + retrieval_func = forms.ChoiceField(choices=[]) + generation_func = forms.ChoiceField(choices=[]) + + @staticmethod + def _available_functions(module: str, prefix: str) -> Generator[tuple[str, str], None, None]: + """ + Import a module and return all functions in it that start with a specific prefix. + + :param module: The name of the module to import. + :param prefix: The prefix of the function names to return. + + :return: A tuple containing the functions that start with the prefix in the module. + """ + # TODO: Implement plugin support for the functions. + _module = importlib.import_module(module) + return ( + (f'{obj.__module__}.{name}', f'{obj.__module__}.{name}') + for name, obj in inspect.getmembers(_module, inspect.isfunction) + if name.startswith(prefix) + ) + def __init__(self, *args, **kwargs): """Initializes the choices for the retrieval and generation function selection fields.""" super().__init__(*args, **kwargs) @@ -111,11 +115,25 @@ class ExternalCertificateAssetAdmin(admin.ModelAdmin): # noqa: D101 prepopulated_fields = {"asset_slug": ("description",)} # noqa: RUF012 -class ExternalCertificateCourseConfigurationForm(forms.ModelForm): # noqa: D101 +class ExternalCertificateCourseConfigurationForm(forms.ModelForm, DocstringOptionsMixin): # noqa: D101 class Meta: # noqa: D106 model = ExternalCertificateCourseConfiguration fields = ('course_id', 'certificate_type', 'custom_options') + def __init__(self, *args, **kwargs): + """Initializes the choices for the retrieval and generation function selection fields.""" + super().__init__(*args, **kwargs) + options = '' + + if self.instance.certificate_type.generation_func: + generation_options = self._get_docstring_custom_options(self.instance.certificate_type.generation_func) + options += generation_options.replace('Custom options:', 'Generation options:') + if self.instance.certificate_type.retrieval_func: + retrieval_options = self._get_docstring_custom_options(self.instance.certificate_type.retrieval_func) + options += retrieval_options.replace('Custom options:', '\nRetrieval options:') + if options: + self.fields['custom_options'].help_text = options.strip() + def clean_course_id(self) -> CourseKey: """Validate the course_id field.""" course_id = self.cleaned_data.get('course_id') From 85854ca7412f874103d6b4c58518d2aa7bfe7b20 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 16 Jan 2024 21:30:34 +0100 Subject: [PATCH 39/46] fixup! feat: display custom options for course configuration in Django admin --- openedx_certificates/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py index 1ad9523..6c2f923 100644 --- a/openedx_certificates/admin.py +++ b/openedx_certificates/admin.py @@ -127,12 +127,12 @@ def __init__(self, *args, **kwargs): if self.instance.certificate_type.generation_func: generation_options = self._get_docstring_custom_options(self.instance.certificate_type.generation_func) - options += generation_options.replace('Custom options:', 'Generation options:') + options += generation_options.replace('Custom options:', '\nGeneration options:') if self.instance.certificate_type.retrieval_func: retrieval_options = self._get_docstring_custom_options(self.instance.certificate_type.retrieval_func) options += retrieval_options.replace('Custom options:', '\nRetrieval options:') - if options: - self.fields['custom_options'].help_text = options.strip() + + self.fields['custom_options'].help_text += options def clean_course_id(self) -> CourseKey: """Validate the course_id field.""" From 17ed355aabc3be424fd99c6f0595a848a6e1ca83 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 17 Jan 2024 02:05:26 +0100 Subject: [PATCH 40/46] fixup! feat: display custom options for course configuration in Django admin --- openedx_certificates/admin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py index 6c2f923..39fb422 100644 --- a/openedx_certificates/admin.py +++ b/openedx_certificates/admin.py @@ -125,14 +125,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) options = '' - if self.instance.certificate_type.generation_func: - generation_options = self._get_docstring_custom_options(self.instance.certificate_type.generation_func) - options += generation_options.replace('Custom options:', '\nGeneration options:') - if self.instance.certificate_type.retrieval_func: - retrieval_options = self._get_docstring_custom_options(self.instance.certificate_type.retrieval_func) - options += retrieval_options.replace('Custom options:', '\nRetrieval options:') - - self.fields['custom_options'].help_text += options + if self.instance.certificate_type: + if self.instance.certificate_type.generation_func: + generation_options = self._get_docstring_custom_options(self.instance.certificate_type.generation_func) + options += generation_options.replace('Custom options:', '\nGeneration options:') + if self.instance.certificate_type.retrieval_func: + retrieval_options = self._get_docstring_custom_options(self.instance.certificate_type.retrieval_func) + options += retrieval_options.replace('Custom options:', '\nRetrieval options:') + + self.fields['custom_options'].help_text += options def clean_course_id(self) -> CourseKey: """Validate the course_id field.""" From 29ffdf3ad7216131a49b33adf1ffa73bd9eda6aa Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 17 Jan 2024 02:08:20 +0100 Subject: [PATCH 41/46] fixup! feat: display custom options for course configuration in Django admin --- openedx_certificates/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_certificates/admin.py b/openedx_certificates/admin.py index 39fb422..7db995c 100644 --- a/openedx_certificates/admin.py +++ b/openedx_certificates/admin.py @@ -125,7 +125,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) options = '' - if self.instance.certificate_type: + if self.instance and getattr(self.instance, 'certificate_type', None): if self.instance.certificate_type.generation_func: generation_options = self._get_docstring_custom_options(self.instance.certificate_type.generation_func) options += generation_options.replace('Custom options:', '\nGeneration options:') From 57b68b22aa8ba78b538b3f71f2f21f796afb2cbd Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 18 Jan 2024 16:47:27 +0100 Subject: [PATCH 42/46] feat: encrypt generated PDF to prevent modifications --- openedx_certificates/generators.py | 10 ++++++++++ tests/test_generators.py | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index df731f1..94c9459 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -11,12 +11,14 @@ import io import logging +import secrets from typing import TYPE_CHECKING, Any from django.conf import settings from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage, default_storage from pypdf import PdfReader, PdfWriter +from pypdf.constants import UserAccessPermissions from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas @@ -127,6 +129,14 @@ def _save_certificate(certificate: PdfWriter, certificate_uuid: UUID) -> str: """ # Save the final PDF file to BytesIO. output_path = f'external_certificates/{certificate_uuid}.pdf' + + view_print_extract_permission = ( + UserAccessPermissions.PRINT + | UserAccessPermissions.PRINT_TO_REPRESENTATION + | UserAccessPermissions.EXTRACT_TEXT_AND_GRAPHICS + ) + certificate.encrypt('', secrets.token_hex(32), permissions_flag=view_print_extract_permission, algorithm='AES-256') + pdf_bytes = io.BytesIO() certificate.write(pdf_bytes) pdf_bytes.seek(0) # Rewind to start. diff --git a/tests/test_generators.py b/tests/test_generators.py index f5bf3d7..8ef24ad 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -13,6 +13,7 @@ from inmemorystorage import InMemoryStorage from opaque_keys.edx.keys import CourseKey from pypdf import PdfWriter +from pypdf.constants import UserAccessPermissions from openedx_certificates.generators import ( _get_user_name, @@ -150,8 +151,9 @@ def test_write_text_on_template(mock_canvas_class: Mock, options: dict[str, int] (Mock(spec=FileSystemStorage, exists=Mock(return_value=True))), ], ) +@patch('openedx_certificates.generators.secrets.token_hex', return_value='test_token') @patch('openedx_certificates.generators.ContentFile', autospec=True) -def test_save_certificate(mock_contentfile: Mock, storage: DefaultStorage | Mock): +def test_save_certificate(mock_contentfile: Mock, mock_token_hex: Mock, storage: DefaultStorage | Mock): """Test the _save_certificate function.""" # Mock the certificate. certificate = Mock(spec=PdfWriter) @@ -162,6 +164,13 @@ def test_save_certificate(mock_contentfile: Mock, storage: DefaultStorage | Mock content_file = ContentFile(pdf_bytes.getvalue()) mock_contentfile.return_value = content_file + # Expected values for the encrypt method + expected_pdf_permissions = ( + UserAccessPermissions.PRINT + | UserAccessPermissions.PRINT_TO_REPRESENTATION + | UserAccessPermissions.EXTRACT_TEXT_AND_GRAPHICS + ) + # Run the function. with patch('openedx_certificates.generators.default_storage', storage): url = _save_certificate(certificate, certificate_uuid) @@ -181,6 +190,14 @@ def test_save_certificate(mock_contentfile: Mock, storage: DefaultStorage | Mock else: assert url == f'/{output_path}' + # Check the calls to certificate.encrypt + certificate.encrypt.assert_called_once_with( + '', + mock_token_hex(), + permissions_flag=expected_pdf_permissions, + algorithm='AES-256', + ) + # Allow specifying a custom domain for certificates. with override_settings(CERTIFICATES_CUSTOM_DOMAIN='https://example2.com'): url = _save_certificate(certificate, certificate_uuid) From 5b31e4dee7768fe7ea3aefe9e5c5b0b3fb32874a Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 23 Jan 2024 22:31:51 +0100 Subject: [PATCH 43/46] test: add migration checks to tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 2cde08d..de01ad7 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,7 @@ deps = -r{toxinidir}/requirements/test.txt commands = python manage.py check + python manage.py makemigrations openedx_certificates --check --dry-run --verbosity 3 pytest {posargs} [testenv:docs] From ff9283809e0f018a4445e8ebdec005205cf82430 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 23 Jan 2024 22:32:03 +0100 Subject: [PATCH 44/46] feat: allow overriding course name --- openedx_certificates/generators.py | 3 ++- tests/test_generators.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index 94c9459..f727eed 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -174,6 +174,7 @@ def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: - font: The name of the font to use. - name_y: The Y coordinate of the name on the certificate (vertical position on the template). - name_color: The color of the name on the certificate (hexadecimal color code). + - course_name: Specify the course name to use instead of the course Display Name retrieved from Open edX. - course_name_y: The Y coordinate of the course name on the certificate (vertical position on the template). - course_name_color: The color of the course name on the certificate (hexadecimal color code). - issue_date_y: The Y coordinate of the issue date on the certificate (vertical position on the template). @@ -184,7 +185,7 @@ def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: template_file = ExternalCertificateAsset.get_asset_by_slug(options['template']) username = _get_user_name(user) - course_name = get_course_name(course_id) + course_name = options.get('course_name') or get_course_name(course_id) # HACK: We support two-line strings by using a semicolon as a separator. if ';' in course_name and (template_path := options.get('template_two-lines')): diff --git a/tests/test_generators.py b/tests/test_generators.py index 8ef24ad..2763359 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -207,13 +207,20 @@ def test_save_certificate(mock_contentfile: Mock, mock_token_hex: Mock, storage: @pytest.mark.parametrize( ("course_name", "options", "expected_template_slug"), [ + # Default. ('Test Course', {'template': 'template_slug'}, 'template_slug'), + # Replace semicolon with newline in course name. ('Test Course;Test Course', {'template': 'template_slug'}, 'template_slug'), + # Specify a different template for two-line course names. ( 'Test Course;Test Course', {'template': 'template_slug', 'template_two-lines': 'template_two_lines_slug'}, 'template_two_lines_slug', ), + # Override course name. + ('Test Course', {'template': 'template_slug', 'course_name': 'Override'}, 'template_slug'), + # Ignore empty course name override. + ('Test Course', {'template': 'template_slug', 'course_name': ''}, 'template_slug'), ], ) @patch( @@ -260,7 +267,10 @@ def test_generate_pdf_certificate( # noqa: PLR0913 assert result == 'certificate_url' mock_get_asset_by_slug.assert_called_with(expected_template_slug) mock_get_user_name.assert_called_once_with(user) - mock_get_course_name.assert_called_once_with(course_id) + if options.get('course_name'): + mock_get_course_name.assert_not_called() + else: + mock_get_course_name.assert_called_once_with(course_id) mock_register_font.assert_called_once_with(options) mock_pdf_reader.assert_called() mock_pdf_writer.assert_called() From c52a65775c45ada0147ed802d05d4139f6c70190 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Fri, 22 Mar 2024 14:57:42 +0100 Subject: [PATCH 45/46] fix: support multiline course names --- openedx_certificates/generators.py | 18 ++++++--- tests/test_generators.py | 59 ++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/openedx_certificates/generators.py b/openedx_certificates/generators.py index f727eed..28ec3fb 100644 --- a/openedx_certificates/generators.py +++ b/openedx_certificates/generators.py @@ -102,9 +102,14 @@ def hex_to_rgb(hex_color: str) -> tuple[float, float, float]: course_name_color = options.get('course_name_color', '#000') pdf_canvas.setFillColorRGB(*hex_to_rgb(course_name_color)) - course_name_x = (template_width - pdf_canvas.stringWidth(course_name)) / 2 course_name_y = options.get('course_name_y', 220) - pdf_canvas.drawString(course_name_x, course_name_y, course_name) + course_name_line_height = 28 * 1.1 + + # Split the course name into lines and write each of them in the center of the template. + for line_number, line in enumerate(course_name.split('\n')): + line_x = (template_width - pdf_canvas.stringWidth(line)) / 2 + line_y = course_name_y - (line_number * course_name_line_height) + pdf_canvas.drawString(line_x, line_y, line) # Write the issue date. issue_date = get_localized_certificate_date() @@ -169,7 +174,7 @@ def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: Options: - template: The path to the PDF template file. - - template_two-lines: The path to the PDF template file for two-line course names. + - template_two_lines: The path to the PDF template file for two-line course names. A two-line course name is specified by using a semicolon as a separator. - font: The name of the font to use. - name_y: The Y coordinate of the name on the certificate (vertical position on the template). @@ -181,16 +186,17 @@ def generate_pdf_certificate(course_id: CourseKey, user: User, certificate_uuid: - issue_date_color: The color of the issue date on the certificate (hexadecimal color code). """ log.info("Starting certificate generation for user %s", user.id) - # Get template from the ExternalCertificateAsset. - template_file = ExternalCertificateAsset.get_asset_by_slug(options['template']) username = _get_user_name(user) course_name = options.get('course_name') or get_course_name(course_id) + # Get template from the ExternalCertificateAsset. # HACK: We support two-line strings by using a semicolon as a separator. - if ';' in course_name and (template_path := options.get('template_two-lines')): + if ';' in course_name and (template_path := options.get('template_two_lines')): template_file = ExternalCertificateAsset.get_asset_by_slug(template_path) course_name = course_name.replace(';', '\n') + else: + template_file = ExternalCertificateAsset.get_asset_by_slug(options['template']) font = _register_font(options) diff --git a/tests/test_generators.py b/tests/test_generators.py index 2763359..c5edabf 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,4 +1,5 @@ """This module contains unit tests for the generate_pdf_certificate function.""" + from __future__ import annotations import io @@ -62,10 +63,11 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas @pytest.mark.parametrize( - ("options", "expected"), + ("course_name", "options", "expected"), [ - ({}, {}), # No options - use default coordinates and colors. + ('Programming 101', {}, {}), # No options - use default coordinates and colors. ( + 'Programming 101', { 'name_y': 250, 'course_name_y': 200, @@ -80,10 +82,11 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas 'issue_date_color': (245 / 255, 154 / 255, 142 / 255), }, ), # Custom coordinates and colors. + ('Programming\n101\nAdvanced Programming', {}, {}), # Multiline course name. ], ) @patch('openedx_certificates.generators.canvas.Canvas', return_value=Mock(stringWidth=Mock(return_value=10))) -def test_write_text_on_template(mock_canvas_class: Mock, options: dict[str, int], expected: dict): +def test_write_text_on_template(mock_canvas_class: Mock, course_name: str, options: dict[str, int], expected: dict): """Test the _write_text_on_template function.""" username = 'John Doe' course_name = 'Programming 101' @@ -123,22 +126,31 @@ def test_write_text_on_template(mock_canvas_class: Mock, options: dict[str, int] expected_course_name_color = expected.get('course_name_color', (0, 0, 0)) expected_issue_date_color = expected.get('issue_date_color', (0, 0, 0)) + # The number of calls to drawString should be 2 (name and issue date) + number of lines in course name. + assert canvas_object.drawString.call_count == 3 + course_name.count('\n') + # Check the calls to setFont, setFillColorRGB and drawString methods on Canvas object assert canvas_object.setFont.call_args_list[0] == call(font, 32) assert canvas_object.setFillColorRGB.call_args_list[0] == call(*expected_name_color) assert canvas_object.drawString.call_args_list[0] == call(expected_name_x, expected_name_y, username) + assert mock_canvas_class.return_value.stringWidth.mock_calls[0][1] == (username,) assert canvas_object.setFont.call_args_list[1] == call(font, 28) assert canvas_object.setFillColorRGB.call_args_list[1] == call(*expected_course_name_color) - assert canvas_object.drawString.call_args_list[1] == call( - expected_course_name_x, - expected_course_name_y, - course_name, - ) assert canvas_object.setFont.call_args_list[2] == call(font, 12) assert canvas_object.setFillColorRGB.call_args_list[2] == call(*expected_issue_date_color) - assert canvas_object.drawString.call_args_list[2] == call(expected_issue_date_x, expected_issue_date_y, test_date) + + for line_number, line in enumerate(course_name.split('\n')): + assert mock_canvas_class.return_value.stringWidth.mock_calls[line_number + 1][1] == (line,) + assert canvas_object.drawString.mock_calls[1 + line_number][1] == ( + expected_course_name_x, + expected_course_name_y - (line_number * 28 * 1.1), + line, + ) + + assert mock_canvas_class.return_value.stringWidth.mock_calls[-1][1] == (test_date,) + assert canvas_object.drawString.mock_calls[-1][1] == (expected_issue_date_x, expected_issue_date_y, test_date) @override_settings(LMS_ROOT_URL="https://example.com", MEDIA_URL="media/") @@ -205,22 +217,23 @@ def test_save_certificate(mock_contentfile: Mock, mock_token_hex: Mock, storage: @pytest.mark.parametrize( - ("course_name", "options", "expected_template_slug"), + ("course_name", "options", "expected_template_slug", "expected_course_name"), [ # Default. - ('Test Course', {'template': 'template_slug'}, 'template_slug'), - # Replace semicolon with newline in course name. - ('Test Course;Test Course', {'template': 'template_slug'}, 'template_slug'), - # Specify a different template for two-line course names. + ('Test Course', {'template': 'template_slug'}, 'template_slug', 'Test Course'), + # Specify a different template for two-line course names and replace semicolon with newline in course name. ( - 'Test Course;Test Course', - {'template': 'template_slug', 'template_two-lines': 'template_two_lines_slug'}, + 'Test Course; Test Course', + {'template': 'template_slug', 'template_two_lines': 'template_two_lines_slug'}, 'template_two_lines_slug', + 'Test Course\n Test Course', ), + # Do not replace semicolon with newline when the `template_two_lines` option is not specified. + ('Test Course; Test Course', {'template': 'template_slug'}, 'template_slug', 'Test Course; Test Course'), # Override course name. - ('Test Course', {'template': 'template_slug', 'course_name': 'Override'}, 'template_slug'), + ('Test Course', {'template': 'template_slug', 'course_name': 'Override'}, 'template_slug', 'Override'), # Ignore empty course name override. - ('Test Course', {'template': 'template_slug', 'course_name': ''}, 'template_slug'), + ('Test Course', {'template': 'template_slug', 'course_name': ''}, 'template_slug', 'Test Course'), ], ) @patch( @@ -256,6 +269,7 @@ def test_generate_pdf_certificate( # noqa: PLR0913 course_name: str, options: dict[str, str], expected_template_slug: str, + expected_course_name: str, ): """Test the generate_pdf_certificate function.""" course_id = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course') @@ -272,7 +286,12 @@ def test_generate_pdf_certificate( # noqa: PLR0913 else: mock_get_course_name.assert_called_once_with(course_id) mock_register_font.assert_called_once_with(options) - mock_pdf_reader.assert_called() - mock_pdf_writer.assert_called() + assert mock_pdf_reader.call_count == 2 + mock_pdf_writer.assert_called_once_with() + mock_write_text_on_template.assert_called_once() + _, args, _kwargs = mock_write_text_on_template.mock_calls[0] + assert args[-2] == expected_course_name + assert args[-1] == options + mock_save_certificate.assert_called_once() From d799a24e1eb35622336fe8b15eefa01f492ab638 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 25 Mar 2024 17:44:00 +0100 Subject: [PATCH 46/46] fix: clean up periodic tasks when deleting course configurations --- openedx_certificates/models.py | 10 ++++++++ pyproject.toml | 1 + tests/test_models.py | 44 ++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/openedx_certificates/models.py b/openedx_certificates/models.py index b03cf69..775092d 100644 --- a/openedx_certificates/models.py +++ b/openedx_certificates/models.py @@ -13,6 +13,8 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import models +from django.db.models.signals import post_delete +from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django_celery_beat.models import IntervalSchedule, PeriodicTask from edx_ace import Message, Recipient, ace @@ -231,6 +233,14 @@ def generate_certificate_for_user(self, user_id: int, celery_task_id: int = 0): certificate.send_email() +# noinspection PyUnusedLocal +@receiver(post_delete, sender=ExternalCertificateCourseConfiguration) +def post_delete_periodic_task(sender, instance, *_args, **_kwargs): # noqa: ANN001, ARG001 + """Delete the associated periodic task when the object is deleted.""" + if instance.periodic_task: + instance.periodic_task.delete() + + class ExternalCertificate(TimeStampedModel): """ Model to represent each individual certificate awarded to a user for a course. diff --git a/pyproject.toml b/pyproject.toml index f72d03d..3a230cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,7 @@ target-version = 'py38' 'ANN205', # missing-return-type-static-method 'INP001', # implicit-namespace-package 'SLF001', # private-member-access + 'RUF018', # assignment-in-assert ] [tool.ruff.flake8-annotations] diff --git a/tests/test_models.py b/tests/test_models.py index ed4e9ea..f07e58d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,6 +8,7 @@ import pytest from django.core.exceptions import ValidationError from django.db import IntegrityError +from django_celery_beat.models import PeriodicTask from openedx_certificates.exceptions import CertificateGenerationError from openedx_certificates.models import ( @@ -18,6 +19,7 @@ from test_utils.factories import UserFactory if TYPE_CHECKING: + from django.db.models import Model from django.contrib.auth.models import User from opaque_keys.edx.keys import CourseKey @@ -102,6 +104,48 @@ def test_periodic_task_is_auto_created(self): assert periodic_task.args == f'[{self.course_config.id}]' assert periodic_task.task == 'openedx_certificates.tasks.generate_certificates_for_course_task' + @pytest.mark.django_db() + def test_periodic_task_is_deleted_on_deletion(self): + """Test that the periodic task is deleted when the configuration is deleted.""" + self.certificate_type.save() + self.course_config.save() + assert PeriodicTask.objects.count() == 1 + + self.course_config.delete() + assert not PeriodicTask.objects.exists() + + @pytest.mark.django_db() + def test_periodic_task_deletion_removes_the_configuration(self): + """Test that the configuration is deleted when the periodic task is deleted.""" + self.certificate_type.save() + self.course_config.save() + assert PeriodicTask.objects.count() == 1 + + self.course_config.periodic_task.delete() + assert not ExternalCertificateCourseConfiguration.objects.exists() + + @pytest.mark.django_db() + @pytest.mark.parametrize( + ("deleted_model", "verified_model"), + [ + (ExternalCertificateCourseConfiguration, PeriodicTask), # `post_delete` signal. + (PeriodicTask, ExternalCertificateCourseConfiguration), # Cascade deletion of the `OneToOneField`. + ], + ) + def test_bulk_delete(self, deleted_model: type[Model], verified_model: type[Model]): + """Test that the bulk deletion of configurations removes the periodic tasks (and vice versa).""" + self.certificate_type.save() + self.course_config.save() + + ExternalCertificateCourseConfiguration( + course_id="course-v1:TestX+T101+2024", + certificate_type=self.certificate_type, + ).save() + assert PeriodicTask.objects.count() == 2 + + deleted_model.objects.all().delete() + assert not verified_model.objects.exists() + def test_str_representation(self): """Test the string representation of the model.""" assert str(self.course_config) == f'{self.certificate_type.name} in course-v1:TestX+T101+2023'

yZ}0YN)8)f+nMlS8E*qpY9GQ_ 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