From d8e91c41a4b25adf42522c576f20738893e994d4 Mon Sep 17 00:00:00 2001 From: edX requirements bot <49161187+edx-requirements-bot@users.noreply.github.com> Date: Mon, 17 Feb 2025 00:34:55 -0500 Subject: [PATCH 01/10] chore: python requirements update (#4573) --- requirements/base.txt | 34 +++++++++++++-------------- requirements/local.txt | 47 ++++++++++++++++++------------------- requirements/production.txt | 34 +++++++++++++-------------- requirements/test.txt | 47 ++++++++++++++++++------------------- 4 files changed, 80 insertions(+), 82 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index ed8db468fb..6f2c6c034f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -41,9 +41,9 @@ beautifulsoup4==4.13.3 # taxonomy-connector billiard==4.2.1 # via celery -boto3==1.36.16 +boto3==1.36.21 # via django-ses -botocore==1.36.16 +botocore==1.36.21 # via # boto3 # s3transfer @@ -92,7 +92,7 @@ code-annotations==2.2.0 # via edx-toggles contentful==2.3.2 # via -r requirements/base.in -cryptography==44.0.0 +cryptography==44.0.1 # via # pyjwt # pyopenssl @@ -157,7 +157,7 @@ django==4.2.19 # xss-utils django-admin-sortable2==2.2.4 # via -r requirements/base.in -django-appconf==1.0.6 +django-appconf==1.1.0 # via django-compressor django-autocomplete-light==3.11.0 # via -r requirements/base.in @@ -169,7 +169,7 @@ django-compressor==4.5.1 # via # -r requirements/base.in # django-libsass -django-config-models==2.7.0 +django-config-models==2.8.0 # via -r requirements/base.in django-contrib-comments==2.2.0 # via -r requirements/base.in @@ -193,7 +193,7 @@ django-elasticsearch-dsl-drf==0.22.5 # via -r requirements/base.in django-extensions==3.2.3 # via -r requirements/base.in -django-filter==24.3 +django-filter==25.1 # via # -r requirements/base.in # taxonomy-connector @@ -235,7 +235,7 @@ django-stdimage==5.3.0 # via # -c requirements/constraints.txt # -r requirements/base.in -django-storages==1.14.4 +django-storages==1.14.5 # via -r requirements/base.in django-taggit==6.1.0 # via @@ -318,11 +318,11 @@ edx-opaque-keys[django]==2.11.0 # edx-drf-extensions # openedx-events # taxonomy-connector -edx-rest-api-client==6.0.0 +edx-rest-api-client==6.1.0 # via # -r requirements/base.in # taxonomy-connector -edx-toggles==5.2.0 +edx-toggles==5.3.0 # via # edx-event-bus-kafka # edx-event-bus-redis @@ -351,7 +351,7 @@ getsmarter-api-clients==0.6.1 # via -r requirements/base.in google-api-core==2.24.1 # via google-api-python-client -google-api-python-client==2.160.0 +google-api-python-client==2.161.0 # via -r requirements/base.in google-auth==2.38.0 # via @@ -366,7 +366,7 @@ google-auth-httplib2==0.2.0 # google-api-python-client google-auth-oauthlib==1.2.1 # via gspread -googleapis-common-protos==1.66.0 +googleapis-common-protos==1.67.0 # via google-api-core gspread==6.1.4 # via -r requirements/base.in @@ -399,7 +399,7 @@ kombu==5.4.2 # via celery libsass==0.23.0 # via django-libsass -lxml[html-clean,html_clean]==5.3.0 +lxml[html-clean,html_clean]==5.3.1 # via # -r requirements/base.in # lxml-html-clean @@ -429,7 +429,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/base.in -openedx-events==9.17.0 +openedx-events==9.18.1 # via # edx-event-bus-kafka # edx-event-bus-redis @@ -464,7 +464,7 @@ protobuf==5.29.3 # google-api-core # googleapis-common-protos # proto-plus -psutil==6.1.1 +psutil==7.0.0 # via edx-django-utils pyasn1==0.6.1 # via @@ -485,7 +485,7 @@ pyjwt[crypto]==2.10.1 # simple-salesforce # snowflake-connector-python # social-auth-core -pymongo==4.11 +pymongo==4.11.1 # via edx-opaque-keys pynacl==1.5.0 # via edx-django-utils @@ -578,11 +578,11 @@ six==1.17.0 # python-dateutil snowflake-connector-python==3.13.2 # via -r requirements/base.in -social-auth-app-django==5.4.2 +social-auth-app-django==5.4.3 # via # -r requirements/base.in # edx-auth-backends -social-auth-core==4.5.4 +social-auth-core==4.5.6 # via # edx-auth-backends # social-auth-app-django diff --git a/requirements/local.txt b/requirements/local.txt index 89fb1d4090..fae9502194 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -88,11 +88,11 @@ boltons==21.0.0 # face # glom # semgrep -boto3==1.36.16 +boto3==1.36.21 # via # -r requirements/test.txt # django-ses -botocore==1.36.16 +botocore==1.36.21 # via # -r requirements/test.txt # boto3 @@ -188,11 +188,11 @@ colorama==0.4.6 # tox contentful==2.3.2 # via -r requirements/test.txt -coverage[toml]==7.6.11 +coverage[toml]==7.6.12 # via # -r requirements/test.txt # pytest-cov -cryptography==44.0.0 +cryptography==44.0.1 # via # -r requirements/test.txt # pyjwt @@ -273,7 +273,7 @@ distlib==0.3.9 # xss-utils django-admin-sortable2==2.2.4 # via -r requirements/test.txt -django-appconf==1.0.6 +django-appconf==1.1.0 # via # -r requirements/test.txt # django-compressor @@ -287,7 +287,7 @@ django-compressor==4.5.1 # via # -r requirements/test.txt # django-libsass -django-config-models==2.7.0 +django-config-models==2.8.0 # via -r requirements/test.txt django-contrib-comments==2.2.0 # via -r requirements/test.txt @@ -319,7 +319,7 @@ django-elasticsearch-dsl-drf==0.22.5 # via -r requirements/test.txt django-extensions==3.2.3 # via -r requirements/test.txt -django-filter==24.3 +django-filter==25.1 # via # -r requirements/test.txt # taxonomy-connector @@ -367,7 +367,7 @@ django-stdimage==5.3.0 # via # -c requirements/constraints.txt # -r requirements/test.txt -django-storages==1.14.4 +django-storages==1.14.5 # via -r requirements/test.txt django-taggit==6.1.0 # via @@ -465,11 +465,11 @@ edx-opaque-keys[django]==2.11.0 # edx-drf-extensions # openedx-events # taxonomy-connector -edx-rest-api-client==6.0.0 +edx-rest-api-client==6.1.0 # via # -r requirements/test.txt # taxonomy-connector -edx-toggles==5.2.0 +edx-toggles==5.3.0 # via # -r requirements/test.txt # edx-event-bus-kafka @@ -499,7 +499,7 @@ face==24.0.0 # glom factory-boy==3.3.3 # via -r requirements/test.txt -faker==35.2.0 +faker==36.1.1 # via # -r requirements/test.txt # factory-boy @@ -530,7 +530,7 @@ google-api-core==2.24.1 # via # -r requirements/test.txt # google-api-python-client -google-api-python-client==2.160.0 +google-api-python-client==2.161.0 # via -r requirements/test.txt google-auth==2.38.0 # via @@ -548,7 +548,7 @@ google-auth-oauthlib==1.2.1 # via # -r requirements/test.txt # gspread -googleapis-common-protos==1.66.0 +googleapis-common-protos==1.67.0 # via # -r requirements/test.txt # google-api-core @@ -624,7 +624,7 @@ libsass==0.23.0 # via # -r requirements/test.txt # django-libsass -lxml[html-clean]==5.3.0 +lxml[html-clean]==5.3.1 # via # -r requirements/test.txt # edx-i18n-tools @@ -683,7 +683,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/test.txt -openedx-events==9.17.0 +openedx-events==9.18.1 # via # -r requirements/test.txt # edx-event-bus-kafka @@ -756,7 +756,7 @@ protobuf==5.29.3 # google-api-core # googleapis-common-protos # proto-plus -psutil==6.1.1 +psutil==7.0.0 # via # -r requirements/test.txt # edx-django-utils @@ -823,7 +823,7 @@ pymemcache==4.0.0 # via # -r requirements/local.in # -r requirements/test.txt -pymongo==4.11 +pymongo==4.11.1 # via # -r requirements/test.txt # edx-opaque-keys @@ -857,7 +857,7 @@ pytest==8.3.4 # pytest-xdist pytest-cov==6.0.0 # via -r requirements/test.txt -pytest-django==4.9.0 +pytest-django==4.10.0 # via -r requirements/test.txt pytest-responses==0.5.1 # via -r requirements/test.txt @@ -873,7 +873,6 @@ python-dateutil==2.9.0.post0 # celery # contentful # elasticsearch-dsl - # faker # freezegun python-memcached==1.62 # via -r requirements/test.txt @@ -1030,11 +1029,11 @@ snowballstemmer==2.2.0 # sphinx snowflake-connector-python==3.13.2 # via -r requirements/test.txt -social-auth-app-django==5.4.2 +social-auth-app-django==5.4.3 # via # -r requirements/test.txt # edx-auth-backends -social-auth-core==4.5.4 +social-auth-core==4.5.6 # via # -r requirements/test.txt # edx-auth-backends @@ -1120,7 +1119,7 @@ tqdm==4.67.1 # via # -r requirements/test.txt # openai -trio==0.28.0 +trio==0.29.0 # via # -r requirements/test.txt # selenium @@ -1136,7 +1135,6 @@ typing-extensions==4.12.2 # beautifulsoup4 # django-countries # edx-opaque-keys - # faker # pydata-sphinx-theme # referencing # semgrep @@ -1146,6 +1144,7 @@ tzdata==2025.1 # via # -r requirements/test.txt # celery + # faker # kombu unicodecsv==0.14.1 # via -r requirements/test.txt @@ -1171,7 +1170,7 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.29.1 +virtualenv==20.29.2 # via # -r requirements/test.txt # tox diff --git a/requirements/production.txt b/requirements/production.txt index fec3c9a3db..5459e31b67 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -56,11 +56,11 @@ billiard==4.2.1 # via # -r requirements/base.txt # celery -boto3==1.36.16 +boto3==1.36.21 # via # -r requirements/base.txt # django-ses -botocore==1.36.16 +botocore==1.36.21 # via # -r requirements/base.txt # boto3 @@ -127,7 +127,7 @@ code-annotations==2.2.0 # edx-toggles contentful==2.3.2 # via -r requirements/base.txt -cryptography==44.0.0 +cryptography==44.0.1 # via # -r requirements/base.txt # pyjwt @@ -196,7 +196,7 @@ django==4.2.19 # xss-utils django-admin-sortable2==2.2.4 # via -r requirements/base.txt -django-appconf==1.0.6 +django-appconf==1.1.0 # via # -r requirements/base.txt # django-compressor @@ -210,7 +210,7 @@ django-compressor==4.5.1 # via # -r requirements/base.txt # django-libsass -django-config-models==2.7.0 +django-config-models==2.8.0 # via -r requirements/base.txt django-contrib-comments==2.2.0 # via -r requirements/base.txt @@ -235,7 +235,7 @@ django-elasticsearch-dsl-drf==0.22.5 # via -r requirements/base.txt django-extensions==3.2.3 # via -r requirements/base.txt -django-filter==24.3 +django-filter==25.1 # via # -r requirements/base.txt # taxonomy-connector @@ -284,7 +284,7 @@ django-stdimage==5.3.0 # via # -c requirements/constraints.txt # -r requirements/base.txt -django-storages==1.14.4 +django-storages==1.14.5 # via -r requirements/base.txt django-taggit==6.1.0 # via @@ -371,11 +371,11 @@ edx-opaque-keys[django]==2.11.0 # edx-drf-extensions # openedx-events # taxonomy-connector -edx-rest-api-client==6.0.0 +edx-rest-api-client==6.1.0 # via # -r requirements/base.txt # taxonomy-connector -edx-toggles==5.2.0 +edx-toggles==5.3.0 # via # -r requirements/base.txt # edx-event-bus-kafka @@ -414,7 +414,7 @@ google-api-core==2.24.1 # via # -r requirements/base.txt # google-api-python-client -google-api-python-client==2.160.0 +google-api-python-client==2.161.0 # via -r requirements/base.txt google-auth==2.38.0 # via @@ -432,7 +432,7 @@ google-auth-oauthlib==1.2.1 # via # -r requirements/base.txt # gspread -googleapis-common-protos==1.66.0 +googleapis-common-protos==1.67.0 # via # -r requirements/base.txt # google-api-core @@ -484,7 +484,7 @@ libsass==0.23.0 # via # -r requirements/base.txt # django-libsass -lxml[html-clean]==5.3.0 +lxml[html-clean]==5.3.1 # via # -r requirements/base.txt # lxml-html-clean @@ -528,7 +528,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/base.txt -openedx-events==9.17.0 +openedx-events==9.18.1 # via # -r requirements/base.txt # edx-event-bus-kafka @@ -575,7 +575,7 @@ protobuf==5.29.3 # google-api-core # googleapis-common-protos # proto-plus -psutil==6.1.1 +psutil==7.0.0 # via # -r requirements/base.txt # edx-django-utils @@ -606,7 +606,7 @@ pyjwt[crypto]==2.10.1 # social-auth-core pymemcache==4.0.0 # via -r requirements/production.in -pymongo==4.11 +pymongo==4.11.1 # via # -r requirements/base.txt # edx-opaque-keys @@ -732,11 +732,11 @@ six==1.17.0 # python-dateutil snowflake-connector-python==3.13.2 # via -r requirements/base.txt -social-auth-app-django==5.4.2 +social-auth-app-django==5.4.3 # via # -r requirements/base.txt # edx-auth-backends -social-auth-core==4.5.4 +social-auth-core==4.5.6 # via # -r requirements/base.txt # edx-auth-backends diff --git a/requirements/test.txt b/requirements/test.txt index 0df83382b4..2bc142d359 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -71,11 +71,11 @@ boltons==21.0.0 # face # glom # semgrep -boto3==1.36.16 +boto3==1.36.21 # via # -r requirements/base.txt # django-ses -botocore==1.36.16 +botocore==1.36.21 # via # -r requirements/base.txt # boto3 @@ -160,11 +160,11 @@ colorama==0.4.6 # tox contentful==2.3.2 # via -r requirements/base.txt -coverage[toml]==7.6.11 +coverage[toml]==7.6.12 # via # -r requirements/test.in # pytest-cov -cryptography==44.0.0 +cryptography==44.0.1 # via # -r requirements/base.txt # pyjwt @@ -240,7 +240,7 @@ django==4.2.19 # xss-utils django-admin-sortable2==2.2.4 # via -r requirements/base.txt -django-appconf==1.0.6 +django-appconf==1.1.0 # via # -r requirements/base.txt # django-compressor @@ -254,7 +254,7 @@ django-compressor==4.5.1 # via # -r requirements/base.txt # django-libsass -django-config-models==2.7.0 +django-config-models==2.8.0 # via -r requirements/base.txt django-contrib-comments==2.2.0 # via -r requirements/base.txt @@ -279,7 +279,7 @@ django-elasticsearch-dsl-drf==0.22.5 # via -r requirements/base.txt django-extensions==3.2.3 # via -r requirements/base.txt -django-filter==24.3 +django-filter==25.1 # via # -r requirements/base.txt # taxonomy-connector @@ -327,7 +327,7 @@ django-stdimage==5.3.0 # via # -c requirements/constraints.txt # -r requirements/base.txt -django-storages==1.14.4 +django-storages==1.14.5 # via -r requirements/base.txt django-taggit==6.1.0 # via @@ -418,11 +418,11 @@ edx-opaque-keys[django]==2.11.0 # edx-drf-extensions # openedx-events # taxonomy-connector -edx-rest-api-client==6.0.0 +edx-rest-api-client==6.1.0 # via # -r requirements/base.txt # taxonomy-connector -edx-toggles==5.2.0 +edx-toggles==5.3.0 # via # -r requirements/base.txt # edx-event-bus-kafka @@ -446,7 +446,7 @@ face==24.0.0 # via glom factory-boy==3.3.3 # via -r requirements/test.in -faker==35.2.0 +faker==36.1.1 # via factory-boy fastavro==1.10.0 # via @@ -473,7 +473,7 @@ google-api-core==2.24.1 # via # -r requirements/base.txt # google-api-python-client -google-api-python-client==2.160.0 +google-api-python-client==2.161.0 # via -r requirements/base.txt google-auth==2.38.0 # via @@ -491,7 +491,7 @@ google-auth-oauthlib==1.2.1 # via # -r requirements/base.txt # gspread -googleapis-common-protos==1.66.0 +googleapis-common-protos==1.67.0 # via # -r requirements/base.txt # google-api-core @@ -552,7 +552,7 @@ libsass==0.23.0 # via # -r requirements/base.txt # django-libsass -lxml[html-clean]==5.3.0 +lxml[html-clean]==5.3.1 # via # -r requirements/base.txt # lxml-html-clean @@ -603,7 +603,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/base.txt -openedx-events==9.17.0 +openedx-events==9.18.1 # via # -r requirements/base.txt # edx-event-bus-kafka @@ -664,7 +664,7 @@ protobuf==5.29.3 # google-api-core # googleapis-common-protos # proto-plus -psutil==6.1.1 +psutil==7.0.0 # via # -r requirements/base.txt # edx-django-utils @@ -713,7 +713,7 @@ pylint-plugin-utils==0.8.2 # pylint-django pymemcache==4.0.0 # via -r requirements/test.in -pymongo==4.11 +pymongo==4.11.1 # via # -r requirements/base.txt # edx-opaque-keys @@ -743,7 +743,7 @@ pytest==8.3.4 # pytest-xdist pytest-cov==6.0.0 # via -r requirements/test.in -pytest-django==4.9.0 +pytest-django==4.10.0 # via -r requirements/test.in pytest-responses==0.5.1 # via -r requirements/test.in @@ -758,7 +758,6 @@ python-dateutil==2.9.0.post0 # celery # contentful # elasticsearch-dsl - # faker # freezegun python-memcached==1.62 # via -r requirements/test.in @@ -895,11 +894,11 @@ sniffio==1.3.1 # via trio snowflake-connector-python==3.13.2 # via -r requirements/base.txt -social-auth-app-django==5.4.2 +social-auth-app-django==5.4.3 # via # -r requirements/base.txt # edx-auth-backends -social-auth-core==4.5.4 +social-auth-core==4.5.6 # via # -r requirements/base.txt # edx-auth-backends @@ -949,7 +948,7 @@ tqdm==4.67.1 # via # -r requirements/base.txt # openai -trio==0.28.0 +trio==0.29.0 # via # selenium # trio-websocket @@ -961,7 +960,6 @@ typing-extensions==4.12.2 # beautifulsoup4 # django-countries # edx-opaque-keys - # faker # referencing # semgrep # simple-salesforce @@ -970,6 +968,7 @@ tzdata==2025.1 # via # -r requirements/base.txt # celery + # faker # kombu unicodecsv==0.14.1 # via -r requirements/base.txt @@ -994,7 +993,7 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.29.1 +virtualenv==20.29.2 # via tox walrus==0.9.4 # via From 178ecb74efd2067d1bf030af3096a4bd67792aaf Mon Sep 17 00:00:00 2001 From: edX requirements bot <49161187+edx-requirements-bot@users.noreply.github.com> Date: Tue, 18 Feb 2025 06:56:07 -0500 Subject: [PATCH 02/10] chore: python requirements update (#4576) --- requirements/base.txt | 6 +++--- requirements/local.txt | 9 +++++---- requirements/production.txt | 6 +++--- requirements/test.txt | 12 +++++++----- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 6f2c6c034f..a374ccdce5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -41,9 +41,9 @@ beautifulsoup4==4.13.3 # taxonomy-connector billiard==4.2.1 # via celery -boto3==1.36.21 +boto3==1.36.22 # via django-ses -botocore==1.36.21 +botocore==1.36.22 # via # boto3 # s3transfer @@ -597,7 +597,7 @@ stevedore==5.4.0 # code-annotations # edx-django-utils # edx-opaque-keys -taxonomy-connector==2.0.0 +taxonomy-connector==2.1.0 # via -r requirements/base.in text-unidecode==1.3 # via python-slugify diff --git a/requirements/local.txt b/requirements/local.txt index fae9502194..c5850603cf 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -88,11 +88,11 @@ boltons==21.0.0 # face # glom # semgrep -boto3==1.36.21 +boto3==1.36.22 # via # -r requirements/test.txt # django-ses -botocore==1.36.21 +botocore==1.36.22 # via # -r requirements/test.txt # boto3 @@ -693,6 +693,7 @@ outcome==1.3.0.post0 # via # -r requirements/test.txt # trio + # trio-websocket packaging==24.2 # via # -r requirements/docs.txt @@ -1091,7 +1092,7 @@ stevedore==5.4.0 # code-annotations # edx-django-utils # edx-opaque-keys -taxonomy-connector==2.0.0 +taxonomy-connector==2.1.0 # via -r requirements/test.txt testfixtures==8.3.0 # via -r requirements/test.txt @@ -1124,7 +1125,7 @@ trio==0.29.0 # -r requirements/test.txt # selenium # trio-websocket -trio-websocket==0.11.1 +trio-websocket==0.12.1 # via # -r requirements/test.txt # selenium diff --git a/requirements/production.txt b/requirements/production.txt index 5459e31b67..755a7507ba 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -56,11 +56,11 @@ billiard==4.2.1 # via # -r requirements/base.txt # celery -boto3==1.36.21 +boto3==1.36.22 # via # -r requirements/base.txt # django-ses -botocore==1.36.21 +botocore==1.36.22 # via # -r requirements/base.txt # boto3 @@ -759,7 +759,7 @@ stevedore==5.4.0 # code-annotations # edx-django-utils # edx-opaque-keys -taxonomy-connector==2.0.0 +taxonomy-connector==2.1.0 # via -r requirements/base.txt text-unidecode==1.3 # via diff --git a/requirements/test.txt b/requirements/test.txt index 2bc142d359..62aa780713 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -71,11 +71,11 @@ boltons==21.0.0 # face # glom # semgrep -boto3==1.36.21 +boto3==1.36.22 # via # -r requirements/base.txt # django-ses -botocore==1.36.21 +botocore==1.36.22 # via # -r requirements/base.txt # boto3 @@ -610,7 +610,9 @@ openedx-events==9.18.1 # edx-event-bus-redis # taxonomy-connector outcome==1.3.0.post0 - # via trio + # via + # trio + # trio-websocket packaging==24.2 # via # -r requirements/base.txt @@ -922,7 +924,7 @@ stevedore==5.4.0 # code-annotations # edx-django-utils # edx-opaque-keys -taxonomy-connector==2.0.0 +taxonomy-connector==2.1.0 # via -r requirements/base.txt testfixtures==8.3.0 # via -r requirements/test.in @@ -952,7 +954,7 @@ trio==0.29.0 # via # selenium # trio-websocket -trio-websocket==0.11.1 +trio-websocket==0.12.1 # via selenium typing-extensions==4.12.2 # via From fc3df5f27117b610d252fccf8ee6cb27a7949b03 Mon Sep 17 00:00:00 2001 From: Usama Sadiq Date: Fri, 21 Feb 2025 15:46:10 +0500 Subject: [PATCH 03/10] refactor: replace deprecated requestmetric middleware (#4578) --- course_discovery/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course_discovery/settings/base.py b/course_discovery/settings/base.py index bb8acf77c1..143f76205a 100644 --- a/course_discovery/settings/base.py +++ b/course_discovery/settings/base.py @@ -118,7 +118,7 @@ 'waffle.middleware.WaffleMiddleware', 'simple_history.middleware.HistoryRequestMiddleware', 'edx_django_utils.cache.middleware.TieredCacheMiddleware', - 'edx_rest_framework_extensions.middleware.RequestMetricsMiddleware', + 'edx_rest_framework_extensions.middleware.RequestCustomAttributesMiddleware', 'edx_rest_framework_extensions.auth.jwt.middleware.EnsureJWTAuthSettingsMiddleware', ) From aaec6c8d6e5eaf03a61b79b9048ec5326a74386e Mon Sep 17 00:00:00 2001 From: Syed Muhammad Dawoud Sheraz Ali <40599381+DawoudSheraz@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:18:48 +0500 Subject: [PATCH 04/10] chore: update owner team information (#4579) * chore: update owner team information --------- Co-authored-by: Feanil Patel --- CODEOWNERS | 1 - catalog-info.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index facc76d58c..784776cf6b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1 @@ # The following users are the owners of all course-discovery files -* @edx/course-discovery-admins diff --git a/catalog-info.yaml b/catalog-info.yaml index 95d4f85cc9..3d4d40711f 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -18,7 +18,7 @@ metadata: openedx.org/arch-interest-groups: "" openedx.org/release: "master" spec: - owner: group:course-discovery-admins + owner: group:2u-phoenix type: 'service' lifecycle: 'production' dependsOn: From 2aaed30d01a544e26cabb4afff6fbec0f79bb0e4 Mon Sep 17 00:00:00 2001 From: Hamza Shafique <41573849+hamza-56@users.noreply.github.com> Date: Sat, 22 Feb 2025 03:56:33 +0500 Subject: [PATCH 05/10] fix: support for french medium point in clean_html (#4577) --- .../apps/course_metadata/tests/test_utils.py | 40 +++++++++++++++---- .../apps/course_metadata/utils.py | 2 - 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/course_discovery/apps/course_metadata/tests/test_utils.py b/course_discovery/apps/course_metadata/tests/test_utils.py index 7e110477a9..5ac6b99eeb 100644 --- a/course_discovery/apps/course_metadata/tests/test_utils.py +++ b/course_discovery/apps/course_metadata/tests/test_utils.py @@ -830,14 +830,8 @@ class UtilsTests(TestCase): ('

qwerty

 

Take Notes

·        To take notes, just tap here and start typing.

·        Or, easily create a digital notebook for all your notes that automatically syncs across your devices, using the free OneNote app.

To learn more and get OneNote, visit www.onenote.com.

 

Heading 2

1.    Ordered item1

2.    Ordered item2

3.    Ordered item3 with italics, bold, underline, strikethrough, all

 

 

 

', """

qwerty

Take Notes

- +

· To take notes, just tap here and start typing.

+

· Or, easily create a digital notebook for all your notes that automatically syncs across your devices, using the free OneNote app.

To learn more and get OneNote, visit www.onenote.com.

Heading 2

    @@ -855,6 +849,36 @@ class UtilsTests(TestCase): ("

    online course.

    Module 1:

    ", """

    online course.

    Module 1:

    """), + + # Make sure asterisk (*) is converted to list items + ( + '

    * Item 1

    * Item 2

    * Item 3

    ', + '
      \n
    • \n

      Item 1

      \n
    • \n
    • \n

      Item 2

      \n
    • \n
    • \n

      Item 3

      \n
    • \n
    ' + ), + ( + '

    Some text before * Item 1

    * Item 2

    * Item 3

    ', + '

    Some text before * Item 1

    \n
      \n
    • \n

      Item 2

      \n
    • \n
    • \n

      Item 3

      \n
    • \n
    ' + ), + + # Make sure French medium dot (·) is preserved and not converted to list items + ( + '

    · Item 1

    · Item 2

    · Item 3

    ', + '

    · Item 1

    \n

    · Item 2

    \n

    · Item 3

    ' + ), + ( + '

    Some text before · Item 1

    · Item 2

    · Item 3

    ', + '

    Some text before · Item 1

    \n

    · Item 2

    \n

    · Item 3

    ' + ), + + # Make sure French medium dots are preserved when mixed with asterisk lists + ( + '

    · Item 1

    * Item 2

    · Item 3

    ', + '

    · Item 1

    \n
      \n
    • Item 2
    • \n
    \n

    · Item 3

    ' + ), + ( + '

    Some text

    · Item 1

    * Item 2

    Regular paragraph

    · Item 3

    ', + '

    Some text

    \n

    · Item 1

    \n
      \n
    • Item 2
    • \n
    \n

    Regular paragraph

    \n

    · Item 3

    ' + ) ) @ddt.unpack def test_clean_html(self, content, expected): diff --git a/course_discovery/apps/course_metadata/utils.py b/course_discovery/apps/course_metadata/utils.py index ad3dbd3f95..603ee1698d 100644 --- a/course_discovery/apps/course_metadata/utils.py +++ b/course_discovery/apps/course_metadata/utils.py @@ -784,8 +784,6 @@ def clean_html(content): is_list_with_dir_attr_present = True cleaned = str(soup) - # Need to re-replace the · middot with the entity so that html2text can transform it to * for
      in markdown - cleaned = cleaned.replace('·', '·') # Need to clean empty and

      tags which are converted to


      by html2text cleaned = cleaned.replace('

      ', '') html_converter = HTML2TextWithLangSpans(bodywidth=None) From 441dfd94bf6453af14e2a9fe3dbfcfd79c0583bf Mon Sep 17 00:00:00 2001 From: edX requirements bot <49161187+edx-requirements-bot@users.noreply.github.com> Date: Mon, 24 Feb 2025 00:44:45 -0500 Subject: [PATCH 06/10] chore: python requirements update (#4580) --- requirements/base.txt | 14 +++++++------- requirements/local.txt | 16 ++++++++-------- requirements/production.txt | 14 +++++++------- requirements/test.txt | 16 ++++++++-------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index a374ccdce5..5ecd0baaa4 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -41,13 +41,13 @@ beautifulsoup4==4.13.3 # taxonomy-connector billiard==4.2.1 # via celery -boto3==1.36.22 +boto3==1.36.26 # via django-ses -botocore==1.36.22 +botocore==1.36.26 # via # boto3 # s3transfer -cachetools==5.5.1 +cachetools==5.5.2 # via google-auth cairocffi==1.4.0 # via @@ -366,7 +366,7 @@ google-auth-httplib2==0.2.0 # google-api-python-client google-auth-oauthlib==1.2.1 # via gspread -googleapis-common-protos==1.67.0 +googleapis-common-protos==1.68.0 # via google-api-core gspread==6.1.4 # via -r requirements/base.in @@ -429,7 +429,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/base.in -openedx-events==9.18.1 +openedx-events==9.18.2 # via # edx-event-bus-kafka # edx-event-bus-redis @@ -453,7 +453,7 @@ platformdirs==4.3.6 # zeep prompt-toolkit==3.0.50 # via click-repl -propcache==0.2.1 +propcache==0.3.0 # via # aiohttp # yarl @@ -592,7 +592,7 @@ soupsieve==2.6 # via beautifulsoup4 sqlparse==0.5.3 # via django -stevedore==5.4.0 +stevedore==5.4.1 # via # code-annotations # edx-django-utils diff --git a/requirements/local.txt b/requirements/local.txt index c5850603cf..088b0ead1c 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -88,11 +88,11 @@ boltons==21.0.0 # face # glom # semgrep -boto3==1.36.22 +boto3==1.36.26 # via # -r requirements/test.txt # django-ses -botocore==1.36.22 +botocore==1.36.26 # via # -r requirements/test.txt # boto3 @@ -101,7 +101,7 @@ bracex==2.5.post1 # via # -r requirements/test.txt # wcmatch -cachetools==5.5.1 +cachetools==5.5.2 # via # -r requirements/test.txt # google-auth @@ -548,7 +548,7 @@ google-auth-oauthlib==1.2.1 # via # -r requirements/test.txt # gspread -googleapis-common-protos==1.67.0 +googleapis-common-protos==1.68.0 # via # -r requirements/test.txt # google-api-core @@ -683,7 +683,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/test.txt -openedx-events==9.18.1 +openedx-events==9.18.2 # via # -r requirements/test.txt # edx-event-bus-kafka @@ -742,7 +742,7 @@ prompt-toolkit==3.0.50 # via # -r requirements/test.txt # click-repl -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/test.txt # aiohttp @@ -973,7 +973,7 @@ rjsmin==1.2.2 # via # -r requirements/test.txt # django-compressor -rpds-py==0.22.3 +rpds-py==0.23.1 # via # -r requirements/test.txt # jsonschema @@ -1086,7 +1086,7 @@ sqlparse==0.5.3 # -r requirements/test.txt # django # django-debug-toolbar -stevedore==5.4.0 +stevedore==5.4.1 # via # -r requirements/test.txt # code-annotations diff --git a/requirements/production.txt b/requirements/production.txt index 755a7507ba..c170f7cf4b 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -56,16 +56,16 @@ billiard==4.2.1 # via # -r requirements/base.txt # celery -boto3==1.36.22 +boto3==1.36.26 # via # -r requirements/base.txt # django-ses -botocore==1.36.22 +botocore==1.36.26 # via # -r requirements/base.txt # boto3 # s3transfer -cachetools==5.5.1 +cachetools==5.5.2 # via # -r requirements/base.txt # google-auth @@ -432,7 +432,7 @@ google-auth-oauthlib==1.2.1 # via # -r requirements/base.txt # gspread -googleapis-common-protos==1.67.0 +googleapis-common-protos==1.68.0 # via # -r requirements/base.txt # google-api-core @@ -528,7 +528,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/base.txt -openedx-events==9.18.1 +openedx-events==9.18.2 # via # -r requirements/base.txt # edx-event-bus-kafka @@ -560,7 +560,7 @@ prompt-toolkit==3.0.50 # via # -r requirements/base.txt # click-repl -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/base.txt # aiohttp @@ -753,7 +753,7 @@ sqlparse==0.5.3 # via # -r requirements/base.txt # django -stevedore==5.4.0 +stevedore==5.4.1 # via # -r requirements/base.txt # code-annotations diff --git a/requirements/test.txt b/requirements/test.txt index 62aa780713..1fde4e75f9 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -71,18 +71,18 @@ boltons==21.0.0 # face # glom # semgrep -boto3==1.36.22 +boto3==1.36.26 # via # -r requirements/base.txt # django-ses -botocore==1.36.22 +botocore==1.36.26 # via # -r requirements/base.txt # boto3 # s3transfer bracex==2.5.post1 # via wcmatch -cachetools==5.5.1 +cachetools==5.5.2 # via # -r requirements/base.txt # google-auth @@ -491,7 +491,7 @@ google-auth-oauthlib==1.2.1 # via # -r requirements/base.txt # gspread -googleapis-common-protos==1.67.0 +googleapis-common-protos==1.68.0 # via # -r requirements/base.txt # google-api-core @@ -603,7 +603,7 @@ openai==0.28.1 # taxonomy-connector openedx-atlas==0.6.2 # via -r requirements/base.txt -openedx-events==9.18.1 +openedx-events==9.18.2 # via # -r requirements/base.txt # edx-event-bus-kafka @@ -651,7 +651,7 @@ prompt-toolkit==3.0.50 # via # -r requirements/base.txt # click-repl -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/base.txt # aiohttp @@ -851,7 +851,7 @@ rjsmin==1.2.2 # via # -r requirements/base.txt # django-compressor -rpds-py==0.22.3 +rpds-py==0.23.1 # via # jsonschema # referencing @@ -918,7 +918,7 @@ sqlparse==0.5.3 # via # -r requirements/base.txt # django -stevedore==5.4.0 +stevedore==5.4.1 # via # -r requirements/base.txt # code-annotations From 97479a74244058ba5aa8420492998697eaf5a633 Mon Sep 17 00:00:00 2001 From: Ali Akbar <52413434+Ali-D-Akbar@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:27:20 +0500 Subject: [PATCH 07/10] feat: send email to the enabled users only (#4574) --- course_discovery/apps/tagging/emails.py | 16 ++++++++--- .../apps/tagging/tests/test_emails.py | 27 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/course_discovery/apps/tagging/emails.py b/course_discovery/apps/tagging/emails.py index 4524d6032a..75d7ef8bb2 100644 --- a/course_discovery/apps/tagging/emails.py +++ b/course_discovery/apps/tagging/emails.py @@ -5,6 +5,8 @@ from django.template.loader import get_template from django.urls import reverse +from course_discovery.apps.publisher.utils import is_email_notification_enabled + logger = logging.getLogger(__name__) @@ -37,8 +39,16 @@ def send_email_for_course_verticals_update(report, to_users): def send_email_for_course_vertical_assignment(course, to_users): """ Sends an email to specified users requesting action to assign vertical and sub-vertical - for a given course. + for a given course, but only to those who have email notifications enabled. """ + email_enabled_users = [user.email for user in to_users if is_email_notification_enabled(user)] + if not email_enabled_users: + logger.exception( + f"Failed to send vertical assignment email for course '{course.title}' (UUID: {course.uuid})" + f"No recipients found." + ) + return + course_tagging_url = ( f"{settings.DISCOVERY_BASE_URL}{reverse('tagging:course_tagging_detail', kwargs={'uuid': course.uuid})}" ) @@ -51,7 +61,7 @@ def send_email_for_course_vertical_assignment(course, to_users): f"Action Required: Assign Vertical and Sub-vertical for Course '{course.title}'", html_content, settings.PUBLISHER_FROM_EMAIL, - to_users, + email_enabled_users, ) email.content_subtype = "html" @@ -60,5 +70,5 @@ def send_email_for_course_vertical_assignment(course, to_users): except Exception as e: # pylint: disable=broad-except logger.exception( f"Failed to send vertical assignment email for course '{course.title}' (UUID: {course.uuid}) to " - f"recipients {', '.join(to_users)}. Error: {str(e)}" + f"recipients {', '.join(email_enabled_users)}. Error: {str(e)}" ) diff --git a/course_discovery/apps/tagging/tests/test_emails.py b/course_discovery/apps/tagging/tests/test_emails.py index af88e95623..56e2c54a82 100644 --- a/course_discovery/apps/tagging/tests/test_emails.py +++ b/course_discovery/apps/tagging/tests/test_emails.py @@ -5,7 +5,9 @@ from django.core import mail from django.test import TestCase +from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.course_metadata.tests.factories import CourseFactory +from course_discovery.apps.publisher.tests.factories import UserAttributeFactory from course_discovery.apps.tagging.emails import send_email_for_course_vertical_assignment @@ -15,19 +17,21 @@ class VerticalAssignmentEmailTests(TestCase): def setUp(self): self.group_name = settings.VERTICALS_MANAGEMENT_GROUPS[0] self.course = CourseFactory(title="Test Course", draft=False) - self.recipients = ["user1@example.com", "user2@example.com"] + self.user1 = UserFactory(email="user1@example.com") + self.user2 = UserFactory(email="user2@example.com") + self.recipients = [self.user1, self.user2] self.logger = logging.getLogger("course_discovery.apps.tagging.emails") def test_email_sent_to_recipients(self): """ - Test that an email is sent to the specified recipients with the correct subject + Test that an email is sent to the specified recipients with the correct subject. """ send_email_for_course_vertical_assignment(self.course, self.recipients) self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] - self.assertEqual(email.to, self.recipients) + self.assertEqual(email.to, [self.user1.email, self.user2.email]) expected_subject = f"Action Required: Assign Vertical and Sub-vertical for Course '{self.course.title}'" self.assertEqual(email.subject, expected_subject) @@ -42,3 +46,20 @@ def test_email_send_failure_logs_exception(self, mock_send): send_email_for_course_vertical_assignment(self.course, self.recipients) self.assertIn("Failed to send vertical assignment email", log_context.output[0]) + + def test_no_email_sent_when_user_notifications_disabled(self): + """ + Test that if a user has disabled email notifications via their UserAttributes, + then no email is sent. + """ + + disabled_user = UserFactory(email="disabled@example.com") + UserAttributeFactory(user=disabled_user, enable_email_notification=False) + + send_email_for_course_vertical_assignment(self.course, [disabled_user]) + + with self.assertLogs(logger=self.logger, level="ERROR") as log_context: + send_email_for_course_vertical_assignment(self.course, [disabled_user]) + + self.assertEqual(len(mail.outbox), 0) + self.assertIn("No recipients found.", log_context.output[0]) From 7ae2b3b425a6c6e747486451b5c4f29b9ea55b3a Mon Sep 17 00:00:00 2001 From: zawan-ila <87228907+zawan-ila@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:43:21 +0500 Subject: [PATCH 08/10] feat: add support for pathway retirement (#4575) --- course_discovery/apps/api/serializers.py | 1 + .../apps/api/tests/test_serializers.py | 1 + .../api/v1/tests/test_views/test_pathways.py | 31 +++++++++++++--- .../apps/api/v1/views/pathways.py | 3 ++ .../apps/course_metadata/choices.py | 6 ++++ .../migrations/0347_pathway_status.py | 28 +++++++++++++++ .../apps/course_metadata/models.py | 7 +++- .../apps/course_metadata/tests/test_admin.py | 4 ++- docs/decisions/0032-pathway-retirement.rst | 35 +++++++++++++++++++ 9 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 course_discovery/apps/course_metadata/migrations/0347_pathway_status.py create mode 100644 docs/decisions/0032-pathway-retirement.rst diff --git a/course_discovery/apps/api/serializers.py b/course_discovery/apps/api/serializers.py index 2d6ea9a126..bbef4995c4 100644 --- a/course_discovery/apps/api/serializers.py +++ b/course_discovery/apps/api/serializers.py @@ -2372,6 +2372,7 @@ class Meta: 'email', 'programs', 'description', + 'status', 'destination_url', 'pathway_type', 'course_run_statuses', diff --git a/course_discovery/apps/api/tests/test_serializers.py b/course_discovery/apps/api/tests/test_serializers.py index db26b4001d..f013b3dfa5 100644 --- a/course_discovery/apps/api/tests/test_serializers.py +++ b/course_discovery/apps/api/tests/test_serializers.py @@ -1588,6 +1588,7 @@ def test_data(self): 'destination_url': pathway.destination_url, 'pathway_type': pathway.pathway_type, 'course_run_statuses': [], + 'status': 'unpublished' } self.assertDictEqual(serializer.data, expected) diff --git a/course_discovery/apps/api/v1/tests/test_views/test_pathways.py b/course_discovery/apps/api/v1/tests/test_views/test_pathways.py index e6ea22d31a..172972389e 100644 --- a/course_discovery/apps/api/v1/tests/test_views/test_pathways.py +++ b/course_discovery/apps/api/v1/tests/test_views/test_pathways.py @@ -5,6 +5,7 @@ from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory +from course_discovery.apps.course_metadata.choices import PathwayStatus from course_discovery.apps.course_metadata.tests.factories import PathwayFactory, ProgramFactory @@ -36,13 +37,16 @@ def setup(self, client, django_assert_num_queries, partner): self.partner = partner self.request = request + def create_pathway(self, status=PathwayStatus.Unpublished): + pathway = PathwayFactory(partner=self.partner, status=status) + program = ProgramFactory(partner=pathway.partner) + pathway.programs.add(program) + return pathway + def test_pathway_list(self): pathways = [] for _ in range(4): - pathway = PathwayFactory(partner=self.partner) - program = ProgramFactory(partner=pathway.partner) - pathway.programs.add(program) - pathways.append(pathway) + pathways.append(self.create_pathway()) response = self.client.get(self.list_path) assert response.status_code == 200 assert response.data['results'] == self.serialize_pathway(pathways, many=True) @@ -57,3 +61,22 @@ def test_only_matching_partner(self): response = self.client.get(self.list_path) assert response.status_code == 200 assert response.data['results'] == self.serialize_pathway([pathway], many=True) + + @pytest.mark.parametrize("status", [PathwayStatus.Unpublished, PathwayStatus.Published, PathwayStatus.Retired]) + def test_status_filtering(self, status): + published_pathway = self.create_pathway(status=PathwayStatus.Published) + unpublished_pathway = self.create_pathway(status=PathwayStatus.Unpublished) + retired_pathway = self.create_pathway(status=PathwayStatus.Retired) + pathways = [published_pathway, unpublished_pathway, retired_pathway] + + # Simple get returns all Pathways + response = self.client.get(self.list_path) + assert response.status_code == 200 + assert response.data["count"] == 3 + assert response.data['results'] == self.serialize_pathway(pathways, many=True) + + # Adding a query param filters the results + response = self.client.get(self.list_path + f'?status={status}') + assert response.status_code == 200 + assert response.data["count"] == 1 + assert response.data['results'] == self.serialize_pathway([locals()[f"{status}_pathway"]], many=True) diff --git a/course_discovery/apps/api/v1/views/pathways.py b/course_discovery/apps/api/v1/views/pathways.py index 5cbde6d66c..e370c30d4b 100644 --- a/course_discovery/apps/api/v1/views/pathways.py +++ b/course_discovery/apps/api/v1/views/pathways.py @@ -1,4 +1,5 @@ """ Views for accessing Pathway data """ +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets from course_discovery.apps.api import serializers @@ -11,6 +12,8 @@ class PathwayViewSet(CompressedCacheResponseMixin, viewsets.ReadOnlyModelViewSet): permission_classes = (ReadOnlyByPublisherUser,) serializer_class = serializers.PathwaySerializer + filter_backends = (DjangoFilterBackend,) + filterset_fields = ('status',) def get_queryset(self): excluded_restriction_types = get_excluded_restriction_types(self.request) diff --git a/course_discovery/apps/course_metadata/choices.py b/course_discovery/apps/course_metadata/choices.py index c6c1bdee38..b65d21ab50 100644 --- a/course_discovery/apps/course_metadata/choices.py +++ b/course_discovery/apps/course_metadata/choices.py @@ -18,6 +18,12 @@ def REVIEW_STATES(cls): return [cls.LegalReview.value, cls.InternalReview.value] +class PathwayStatus(models.TextChoices): + Unpublished = 'unpublished', _('Unpublished') + Published = 'published', _('Published') + Retired = 'retired', _('Retired') + + class CourseRunPacing(models.TextChoices): # Translators: Instructor-paced refers to course runs that operate on a schedule set by the instructor, # similar to a normal university course. diff --git a/course_discovery/apps/course_metadata/migrations/0347_pathway_status.py b/course_discovery/apps/course_metadata/migrations/0347_pathway_status.py new file mode 100644 index 0000000000..082f5af2dd --- /dev/null +++ b/course_discovery/apps/course_metadata/migrations/0347_pathway_status.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.18 on 2025-02-17 15:50 + +from django.db import migrations, models + +from course_discovery.apps.course_metadata.choices import PathwayStatus + + +def set_status_on_existing_pathways(apps, schema_editor): + # For all existing pathways, we set the status to Published. + # Any deviations from that should be handled manually. + Pathway = apps.get_model('course_metadata', 'Pathway') + Pathway.objects.update(status=PathwayStatus.Published) + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_metadata', '0346_archivecoursesconfig'), + ] + + operations = [ + migrations.AddField( + model_name='pathway', + name='status', + field=models.CharField(choices=[('unpublished', 'Unpublished'), ('published', 'Published'), ('retired', 'Retired')], default='unpublished', max_length=255), + ), + migrations.RunPython(set_status_on_existing_pathways, migrations.RunPython.noop) + ] diff --git a/course_discovery/apps/course_metadata/models.py b/course_discovery/apps/course_metadata/models.py index e11aef7547..c64d71250e 100644 --- a/course_discovery/apps/course_metadata/models.py +++ b/course_discovery/apps/course_metadata/models.py @@ -44,7 +44,7 @@ from course_discovery.apps.course_metadata import emails from course_discovery.apps.course_metadata.choices import ( CertificateType, CourseLength, CourseRunPacing, CourseRunRestrictionType, CourseRunStatus, - ExternalCourseMarketingType, ExternalProductStatus, PayeeType, ProgramStatus, ReportingType + ExternalCourseMarketingType, ExternalProductStatus, PathwayStatus, PayeeType, ProgramStatus, ReportingType ) from course_discovery.apps.course_metadata.constants import SUBDIRECTORY_SLUG_FORMAT_REGEX, PathwayType from course_discovery.apps.course_metadata.fields import AutoSlugWithSlashesField, HtmlField, NullHtmlField @@ -4371,6 +4371,11 @@ class Pathway(TimeStampedModel): choices=[(tag.value, tag.value) for tag in PathwayType], default=PathwayType.CREDIT.value, ) + status = models.CharField( + default=PathwayStatus.Unpublished, + max_length=255, null=False, blank=False, + choices=PathwayStatus.choices + ) def __str__(self): return self.name diff --git a/course_discovery/apps/course_metadata/tests/test_admin.py b/course_discovery/apps/course_metadata/tests/test_admin.py index a2acdea6d3..8ecfbb14bf 100644 --- a/course_discovery/apps/course_metadata/tests/test_admin.py +++ b/course_discovery/apps/course_metadata/tests/test_admin.py @@ -23,7 +23,7 @@ from course_discovery.apps.core.tests.factories import USER_PASSWORD, PartnerFactory, UserFactory from course_discovery.apps.core.tests.helpers import make_image_file from course_discovery.apps.course_metadata.admin import DegreeAdmin, PositionAdmin, ProgramEligibilityFilter -from course_discovery.apps.course_metadata.choices import ProgramStatus +from course_discovery.apps.course_metadata.choices import PathwayStatus, ProgramStatus from course_discovery.apps.course_metadata.constants import PathwayType from course_discovery.apps.course_metadata.forms import PathwayAdminForm, ProgramAdminForm from course_discovery.apps.course_metadata.models import ( @@ -628,6 +628,7 @@ def test_program_with_same_partner(self): 'email': 'email@example.com', 'programs': [program1.id], 'pathway_type': PathwayType.CREDIT.value, + 'status': PathwayStatus.Published } form = PathwayAdminForm(data=data) @@ -650,6 +651,7 @@ def test_program_with_different_partner(self): 'email': 'email@example.com', 'programs': [program1.id, program2.id], 'pathway_type': PathwayType.INDUSTRY.value, + 'status': PathwayStatus.Unpublished } form = PathwayAdminForm(data=data) diff --git a/docs/decisions/0032-pathway-retirement.rst b/docs/decisions/0032-pathway-retirement.rst new file mode 100644 index 0000000000..a6456607dc --- /dev/null +++ b/docs/decisions/0032-pathway-retirement.rst @@ -0,0 +1,35 @@ +32. Retirement Mechanism for Pathways +====================================== + +Status +-------- +Accepted (Feb 2025) + +Context +--------- +Currently, there is no way to mark a pathway as retired. This is often necessary due to changing requirements +for credit recognition on the partner organization's end, or the discontinuation of programs offered by them. +In such cases, these pathways should be hidden from the learner dashboard and any credit requests against them +should not be accepted. + +Decision +---------- +A new field **status** will be added to the pathway model. This field will support three possible values: *unpublished*, +*published* and *retired*. Existing pathways will be assigned the *published* status (through a data migration), while any new pathways will be set +to *unpublished* by default. + +The **status** field will be exposed in the pathways endpoint, and the API will also support filtering by its value. + +Consequences +-------------- +Consuming systems, such as credentials and edx-plaform, will have to ensure that they take the status field in consideration +while processing pathways. Specifically, credentials will need to ensure that it does not allow credit redemption requests +against retired pathways, and edx-platform will need to exclude retired pathways from the programs section of the learner dashboard. + +Alternatives Considered +------------------------- +One alternative considered was to hide retired pathways by default in the API responses. However, this approach +was soon determined to be problematic because it could cause issues on the Credentials side, which has its own +Pathway model (regularly synced with Discovery) having protected constraints with some other models. Additionally, +it is more transparent to place the responsibility of correct usage on the consuming systems, rather than automatically +filtering retired objects on discovery's end. From e0f63ad2757a082984ef0c7130f85c0470d1f0df Mon Sep 17 00:00:00 2001 From: Syed Muhammad Dawoud Sheraz Ali <40599381+DawoudSheraz@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:51:51 +0500 Subject: [PATCH 09/10] fix: send users list in vertical tagging email (#4582) --- course_discovery/apps/tagging/emails.py | 4 ++-- course_discovery/apps/tagging/tests/test_emails.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/course_discovery/apps/tagging/emails.py b/course_discovery/apps/tagging/emails.py index 75d7ef8bb2..0b473b8a4d 100644 --- a/course_discovery/apps/tagging/emails.py +++ b/course_discovery/apps/tagging/emails.py @@ -41,7 +41,7 @@ def send_email_for_course_vertical_assignment(course, to_users): Sends an email to specified users requesting action to assign vertical and sub-vertical for a given course, but only to those who have email notifications enabled. """ - email_enabled_users = [user.email for user in to_users if is_email_notification_enabled(user)] + email_enabled_users = [user for user in to_users if is_email_notification_enabled(user)] if not email_enabled_users: logger.exception( f"Failed to send vertical assignment email for course '{course.title}' (UUID: {course.uuid})" @@ -70,5 +70,5 @@ def send_email_for_course_vertical_assignment(course, to_users): except Exception as e: # pylint: disable=broad-except logger.exception( f"Failed to send vertical assignment email for course '{course.title}' (UUID: {course.uuid}) to " - f"recipients {', '.join(email_enabled_users)}. Error: {str(e)}" + f"recipients {', '.join(list(map(lambda user: user.email, email_enabled_users)))}. Error: {str(e)}" ) diff --git a/course_discovery/apps/tagging/tests/test_emails.py b/course_discovery/apps/tagging/tests/test_emails.py index 56e2c54a82..ed8186e1f3 100644 --- a/course_discovery/apps/tagging/tests/test_emails.py +++ b/course_discovery/apps/tagging/tests/test_emails.py @@ -31,7 +31,7 @@ def test_email_sent_to_recipients(self): self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] - self.assertEqual(email.to, [self.user1.email, self.user2.email]) + self.assertEqual(email.to, [self.user1, self.user2]) expected_subject = f"Action Required: Assign Vertical and Sub-vertical for Course '{self.course.title}'" self.assertEqual(email.subject, expected_subject) From 417de032058d059201964733d7eeb98570eb7253 Mon Sep 17 00:00:00 2001 From: Syed Muhammad Dawoud Sheraz Ali <40599381+DawoudSheraz@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:16:47 +0500 Subject: [PATCH 10/10] fix: ensure users are passed to tagging email util (#4583) --- course_discovery/apps/tagging/emails.py | 8 ++++++-- course_discovery/apps/tagging/signals.py | 9 +++++---- course_discovery/apps/tagging/tests/test_emails.py | 2 +- course_discovery/apps/tagging/tests/test_signals.py | 4 ++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/course_discovery/apps/tagging/emails.py b/course_discovery/apps/tagging/emails.py index 0b473b8a4d..005d947c83 100644 --- a/course_discovery/apps/tagging/emails.py +++ b/course_discovery/apps/tagging/emails.py @@ -40,8 +40,12 @@ def send_email_for_course_vertical_assignment(course, to_users): """ Sends an email to specified users requesting action to assign vertical and sub-vertical for a given course, but only to those who have email notifications enabled. + + Arguments: + course(Object): course model instance + to_users(List): list of user objects """ - email_enabled_users = [user for user in to_users if is_email_notification_enabled(user)] + email_enabled_users = [user.email for user in to_users if is_email_notification_enabled(user)] if not email_enabled_users: logger.exception( f"Failed to send vertical assignment email for course '{course.title}' (UUID: {course.uuid})" @@ -70,5 +74,5 @@ def send_email_for_course_vertical_assignment(course, to_users): except Exception as e: # pylint: disable=broad-except logger.exception( f"Failed to send vertical assignment email for course '{course.title}' (UUID: {course.uuid}) to " - f"recipients {', '.join(list(map(lambda user: user.email, email_enabled_users)))}. Error: {str(e)}" + f"recipients {', '.join(email_enabled_users)}. Error: {str(e)}" ) diff --git a/course_discovery/apps/tagging/signals.py b/course_discovery/apps/tagging/signals.py index 1c7705f47a..381d2d4450 100644 --- a/course_discovery/apps/tagging/signals.py +++ b/course_discovery/apps/tagging/signals.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.contrib.auth.models import Group +from django.contrib.auth import get_user_model from django.db.models.signals import post_save from django.dispatch import receiver @@ -16,10 +16,11 @@ def notify_vertical_assignment(instance, created, **kwargs): if instance.draft or not created: return + User = get_user_model() management_groups = getattr(settings, "VERTICALS_MANAGEMENT_GROUPS", []) - groups = Group.objects.filter(name__in=management_groups).exclude(user__isnull=True) - recipients = set(groups.values_list('user__email', flat=True)) - + recipients = list( + User.objects.prefetch_related('groups').filter(groups__name__in=management_groups).distinct() + ) if recipients: send_email_for_course_vertical_assignment(instance, recipients) diff --git a/course_discovery/apps/tagging/tests/test_emails.py b/course_discovery/apps/tagging/tests/test_emails.py index ed8186e1f3..56e2c54a82 100644 --- a/course_discovery/apps/tagging/tests/test_emails.py +++ b/course_discovery/apps/tagging/tests/test_emails.py @@ -31,7 +31,7 @@ def test_email_sent_to_recipients(self): self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] - self.assertEqual(email.to, [self.user1, self.user2]) + self.assertEqual(email.to, [self.user1.email, self.user2.email]) expected_subject = f"Action Required: Assign Vertical and Sub-vertical for Course '{self.course.title}'" self.assertEqual(email.subject, expected_subject) diff --git a/course_discovery/apps/tagging/tests/test_signals.py b/course_discovery/apps/tagging/tests/test_signals.py index e7b4103136..e005c47ea1 100644 --- a/course_discovery/apps/tagging/tests/test_signals.py +++ b/course_discovery/apps/tagging/tests/test_signals.py @@ -34,11 +34,11 @@ def test_notify_vertical_assignment_email_sent(self, mock_send_email): course_run.status = CourseRunStatus.Reviewed course_run.save() - expected_recipients = {"user1@example.com", "user2@example.com"} + expected_recipients = [user1, user2] mock_send_email.assert_called_once() called_args = mock_send_email.call_args[0] self.assertEqual(called_args[0].uuid, course.uuid) - self.assertEqual(called_args[1], expected_recipients) + self.assertListEqual(called_args[1], expected_recipients) @mock.patch("course_discovery.apps.tagging.signals.send_email_for_course_vertical_assignment") def test_notify_vertical_assignment_email_when_course_is_draft(self, mock_send_email):