From 45e4966ae2ea8c3242eaa093f43b0ca82048538f Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 4 Dec 2020 13:05:26 +0500 Subject: [PATCH 01/18] MCKIN-24384: bump xblock-adventure version (#2019) * bump xblock adventure version * bump xblock-utils --- requirements/edx/base.in | 3 ++- requirements/edx/base.txt | 2 +- requirements/edx/custom.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/testing.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements/edx/base.in b/requirements/edx/base.in index f747ec2d55b5..26b696881306 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -154,5 +154,6 @@ user-util # Functionality for retiring users (GDPR com web-fragments # Provides the ability to render fragments of web pages XBlock # Courseware component architecture xblock-review # XBlock which displays problems from earlier in the course for ungraded retries -xblock-utils # Provides utilities used by the Discussion XBlock +# Using this forked version of xblock-utils because of a fix related to i18n in master but master do not support python 2 +-e git+https://github.com/msaqib52/xblock-utils.git@a05d63cf5e07d0920a2dbd66032dcf579e501d4d#egg=xblock-utils==1.2.3 zendesk # Python API for the Zendesk customer support system diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index ae7b63835019..e3aa439542d2 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -37,6 +37,7 @@ git+https://github.com/edx/RecommenderXBlock.git@1.4.0#egg=recommender-xblock==1 git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-drag-and-drop-v2==2.1.6 -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1 +-e git+https://github.com/msaqib52/xblock-utils.git@a05d63cf5e07d0920a2dbd66032dcf579e501d4d#egg=xblock-utils==1.2.3 -e common/lib/xmodule amqp==1.4.9 # via kombu analytics-python==1.2.9 @@ -248,6 +249,5 @@ webencodings==0.5.1 # via html5lib webob==1.8.5 # via xblock wrapt==1.10.5 xblock-review==1.1.5 -xblock-utils==1.2.2 xblock==1.2.2 zendesk==1.1.1 diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index fca0f3474284..7a082b7937b8 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -11,7 +11,7 @@ -e git+https://github.com/open-craft/xblock-drag-and-drop-v2.git@82c9dc5e16d10793e8b79e60661e1a78893fce25#egg=xblock-drag-and-drop-v2-new -e git+https://github.com/edx-solutions/xblock-ooyala.git@v4.0.5#egg=xblock-ooyala==4.0.5 git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-group-project==0.1.3 --e git+https://github.com/edx-solutions/xblock-adventure.git@0.4#egg=xblock-adventure==0.4 +-e git+https://github.com/edx-solutions/xblock-adventure.git@0.4.1#egg=xblock-adventure==0.4.1 -e git+https://github.com/open-craft/xblock-poll.git@v1.10.1#egg=xblock-poll==1.10.1 -e git+https://github.com/open-craft/problem-builder.git@v3.5.7#egg=xblock-problem-builder==3.5.7 -e git+https://github.com/OfficeDev/xblock-officemix.git@86238f5968a08db005717dbddc346808f1ed3716#egg=xblock-officemix diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 85fd6b711da8..6deee59bd408 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -40,6 +40,7 @@ git+https://github.com/edx/RecommenderXBlock.git@1.4.0#egg=recommender-xblock==1 git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-drag-and-drop-v2==2.1.6 -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1 +-e git+https://github.com/msaqib52/xblock-utils.git@a05d63cf5e07d0920a2dbd66032dcf579e501d4d#egg=xblock-utils==1.2.3 -e common/lib/xmodule alabaster==0.7.12 # via sphinx amqp==1.4.9 @@ -351,7 +352,6 @@ webob==1.8.5 werkzeug==0.14.1 wrapt==1.10.5 xblock-review==1.1.5 -xblock-utils==1.2.2 xblock==1.2.2 xmltodict==0.11.0 zendesk==1.1.1 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 5a3ce03823a4..47b0ba82af20 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -38,6 +38,7 @@ git+https://github.com/edx/RecommenderXBlock.git@1.4.0#egg=recommender-xblock==1 git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-drag-and-drop-v2==2.1.6 -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1 +-e git+https://github.com/msaqib52/xblock-utils.git@a05d63cf5e07d0920a2dbd66032dcf579e501d4d#egg=xblock-utils==1.2.3 -e common/lib/xmodule amqp==1.4.9 analytics-python==1.2.9 @@ -334,7 +335,6 @@ webob==1.8.5 werkzeug==0.14.1 # via flask wrapt==1.10.5 xblock-review==1.1.5 -xblock-utils==1.2.2 xblock==1.2.2 xmltodict==0.11.0 # via moto zendesk==1.1.1 From 35f3fe384f046303aa008055f621e03b589a5783 Mon Sep 17 00:00:00 2001 From: Moeez Zahid Date: Thu, 3 Dec 2020 18:24:37 +0500 Subject: [PATCH 02/18] MCKIN-25650 Ooyala version bump (#2014) --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index 7a082b7937b8..877c1629ac68 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -9,7 +9,7 @@ # This is required for A2E courses that were created with the temporary (xblock-drag-and-drop-v2-new) DnDv2 branch to continue to work. # FIXME: bump version to 2.1.6 when https://github.com/edx-solutions/xblock-drag-and-drop-v2/pull/154 merged and tagged -e git+https://github.com/open-craft/xblock-drag-and-drop-v2.git@82c9dc5e16d10793e8b79e60661e1a78893fce25#egg=xblock-drag-and-drop-v2-new --e git+https://github.com/edx-solutions/xblock-ooyala.git@v4.0.5#egg=xblock-ooyala==4.0.5 +-e git+https://github.com/edx-solutions/xblock-ooyala.git@v4.1.1#egg=xblock-ooyala==4.1.1 git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-group-project==0.1.3 -e git+https://github.com/edx-solutions/xblock-adventure.git@0.4.1#egg=xblock-adventure==0.4.1 -e git+https://github.com/open-craft/xblock-poll.git@v1.10.1#egg=xblock-poll==1.10.1 From 2efc631c829e7b8526c9de4249b207b85760b527 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 10 Dec 2020 12:04:16 +0500 Subject: [PATCH 03/18] MCKIN-27715: bump edx-notifications version to 2.0.1 (#2025) --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index 877c1629ac68..9095716a004e 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -21,7 +21,7 @@ git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-g -e git+https://github.com/mckinseyacademy/xblock-diagnosticfeedback.git@v0.4.1#egg=xblock-diagnostic-feedback==0.4.1 -e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.7#egg=xblock-group-project-v2==0.10.7 -e git+https://github.com/open-craft/xblock-virtualreality.git@v0.1.5#egg=xblock-virtualreality==0.1.5 --e git+https://github.com/edx/edx-notifications.git@2.0.0#egg=edx-notifications==2.0.0 +-e git+https://github.com/edx/edx-notifications.git@2.0.1#egg=edx-notifications==2.0.1 git+https://github.com/edx-solutions/gradebook-edx-platform-extensions.git@2.0.2#egg=gradebook-edx-platform-extensions==2.0.2 git+https://github.com/edx-solutions/mobileapps-edx-platform-extensions.git@v2.0.0#egg=mobileapps-edx-platform-extensions==2.0.0 From 59a8f8f00806e3387d21b89fb1c7bec606c82e5f Mon Sep 17 00:00:00 2001 From: Muhammad Usman <43761905+musmanmalik@users.noreply.github.com> Date: Wed, 9 Dec 2020 18:32:23 +0500 Subject: [PATCH 04/18] vb gp (#2024) --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index 9095716a004e..ea7c6285d42b 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -19,7 +19,7 @@ git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-g -e git+https://github.com/open-craft/xblock-eoc-journal.git@v0.9.4#egg=xblock-eoc-journal==0.9.4 -e git+https://github.com/mckinseyacademy/xblock-scorm.git@v3.2.1#egg=xblock-scorm==3.2.1 -e git+https://github.com/mckinseyacademy/xblock-diagnosticfeedback.git@v0.4.1#egg=xblock-diagnostic-feedback==0.4.1 --e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.7#egg=xblock-group-project-v2==0.10.7 +-e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.8#egg=xblock-group-project-v2==0.10.8 -e git+https://github.com/open-craft/xblock-virtualreality.git@v0.1.5#egg=xblock-virtualreality==0.1.5 -e git+https://github.com/edx/edx-notifications.git@2.0.1#egg=edx-notifications==2.0.1 From 987d0c5b5667e6e7447deac0207b9ab3a700bf8b Mon Sep 17 00:00:00 2001 From: Muhammad Usman <43761905+musmanmalik@users.noreply.github.com> Date: Mon, 14 Dec 2020 13:49:06 +0500 Subject: [PATCH 05/18] VB GP v2 (#2029) --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index ea7c6285d42b..d66bd893ff8e 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -19,7 +19,7 @@ git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-g -e git+https://github.com/open-craft/xblock-eoc-journal.git@v0.9.4#egg=xblock-eoc-journal==0.9.4 -e git+https://github.com/mckinseyacademy/xblock-scorm.git@v3.2.1#egg=xblock-scorm==3.2.1 -e git+https://github.com/mckinseyacademy/xblock-diagnosticfeedback.git@v0.4.1#egg=xblock-diagnostic-feedback==0.4.1 --e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.8#egg=xblock-group-project-v2==0.10.8 +-e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.9#egg=xblock-group-project-v2==0.10.9 -e git+https://github.com/open-craft/xblock-virtualreality.git@v0.1.5#egg=xblock-virtualreality==0.1.5 -e git+https://github.com/edx/edx-notifications.git@2.0.1#egg=edx-notifications==2.0.1 From 7f514ba3332f9ef93eaabecc177562f6f21cae6f Mon Sep 17 00:00:00 2001 From: Nasir Hussain Date: Mon, 14 Dec 2020 18:37:44 +0500 Subject: [PATCH 06/18] Release V1.58.0 --- requirements/edx/custom.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index d66bd893ff8e..546f4d45ef6e 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -9,9 +9,9 @@ # This is required for A2E courses that were created with the temporary (xblock-drag-and-drop-v2-new) DnDv2 branch to continue to work. # FIXME: bump version to 2.1.6 when https://github.com/edx-solutions/xblock-drag-and-drop-v2/pull/154 merged and tagged -e git+https://github.com/open-craft/xblock-drag-and-drop-v2.git@82c9dc5e16d10793e8b79e60661e1a78893fce25#egg=xblock-drag-and-drop-v2-new --e git+https://github.com/edx-solutions/xblock-ooyala.git@v4.1.1#egg=xblock-ooyala==4.1.1 +-e git+https://github.com/edx-solutions/xblock-ooyala.git@v4.1.2#egg=xblock-ooyala==4.1.2 git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-group-project==0.1.3 --e git+https://github.com/edx-solutions/xblock-adventure.git@0.4.1#egg=xblock-adventure==0.4.1 +-e git+https://github.com/edx-solutions/xblock-adventure.git@0.4.3#egg=xblock-adventure==0.4.3 -e git+https://github.com/open-craft/xblock-poll.git@v1.10.1#egg=xblock-poll==1.10.1 -e git+https://github.com/open-craft/problem-builder.git@v3.5.7#egg=xblock-problem-builder==3.5.7 -e git+https://github.com/OfficeDev/xblock-officemix.git@86238f5968a08db005717dbddc346808f1ed3716#egg=xblock-officemix @@ -29,7 +29,7 @@ git+https://github.com/edx-solutions/discussion-edx-platform-extensions.git@v2.0 git+https://github.com/edx-solutions/organizations-edx-platform-extensions.git@v2.0.7#egg=organizations-edx-platform-extensions==2.0.7 git+https://github.com/edx-solutions/course-edx-platform-extensions.git@v3.0.0#egg=course-edx-platform-extensions==3.0.0 git+https://github.com/edx-solutions/projects-edx-platform-extensions.git@v3.0.4#egg=projects-edx-platform-extensions==3.0.4 --e git+https://github.com/edx-solutions/api-integration.git@3b19d4e63c2ff683e7680347fe9f1989330b8be7#egg=api-integration +-e git+https://github.com/edx-solutions/api-integration.git@v4.1.6#egg=api-integration==4.1.6 git+https://github.com/mckinseyacademy/openedx-user-manager-api@v1.2.0#egg=openedx-user-manager-api==1.2.0 openedx-completion-aggregator==2.2.4 From 3b3845eb27fd74ac4667491a36260b1fbcb83f43 Mon Sep 17 00:00:00 2001 From: Muhammad Usman <43761905+musmanmalik@users.noreply.github.com> Date: Mon, 14 Dec 2020 12:30:07 +0500 Subject: [PATCH 07/18] Reviewer Feedback Added (#2027) --- conf/locale/zh_CN/LC_MESSAGES/django.mo | Bin 602231 -> 602228 bytes conf/locale/zh_CN/LC_MESSAGES/django.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.mo b/conf/locale/zh_CN/LC_MESSAGES/django.mo index 0ccf3f084aaee55430e1063ba25c1badd6577a03..7d34628daea8f2da74eba3d6d7869af1f6bbc26d 100644 GIT binary patch delta 26176 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 O`7dI7xw)Ytv;Pk(0QJ@Y delta 26179 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 diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.po b/conf/locale/zh_CN/LC_MESSAGES/django.po index ed21f022ce71..bb4d0a6ce896 100644 --- a/conf/locale/zh_CN/LC_MESSAGES/django.po +++ b/conf/locale/zh_CN/LC_MESSAGES/django.po @@ -24601,7 +24601,7 @@ msgid "" "File type '%(detected_type)s' is not allowed. Allowed types are: " "'%(allowed_types)s'." msgstr "" -"文件类型“%(detected_type)s”不受支持。支持的类型为:“%(allowed_types)s”。" +"不支持文件类型“%(detected_type)s”。支持的类型为:“%(allowed_types)s”。" #: lms/lib/utils.py:13 #, python-format From 34e8ad6f25992949c5150cde7a727e6a063ca2e2 Mon Sep 17 00:00:00 2001 From: Muhammad Usman <43761905+musmanmalik@users.noreply.github.com> Date: Wed, 16 Dec 2020 15:21:16 +0500 Subject: [PATCH 08/18] VB GP v2 (#2033) --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index 546f4d45ef6e..4adde42025b2 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -19,7 +19,7 @@ git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-g -e git+https://github.com/open-craft/xblock-eoc-journal.git@v0.9.4#egg=xblock-eoc-journal==0.9.4 -e git+https://github.com/mckinseyacademy/xblock-scorm.git@v3.2.1#egg=xblock-scorm==3.2.1 -e git+https://github.com/mckinseyacademy/xblock-diagnosticfeedback.git@v0.4.1#egg=xblock-diagnostic-feedback==0.4.1 --e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.9#egg=xblock-group-project-v2==0.10.9 +-e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.10#egg=xblock-group-project-v2==0.10.10 -e git+https://github.com/open-craft/xblock-virtualreality.git@v0.1.5#egg=xblock-virtualreality==0.1.5 -e git+https://github.com/edx/edx-notifications.git@2.0.1#egg=edx-notifications==2.0.1 From 845aa0867c9d41662f3ab0427a06a451065c1ecf Mon Sep 17 00:00:00 2001 From: ahmed-zubair12 <74174850+ahmed-zubair12@users.noreply.github.com> Date: Fri, 11 Dec 2020 20:01:17 +0500 Subject: [PATCH 09/18] MCKIN-24991 Increased expiry time of s3 csv file link to 20 minutes (#2028) --- lms/djangoapps/instructor_task/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py index 0a8d57f7eb67..d7f13cf82383 100644 --- a/lms/djangoapps/instructor_task/models.py +++ b/lms/djangoapps/instructor_task/models.py @@ -204,7 +204,7 @@ def from_config(cls, config_name): 'bucket': config['BUCKET'], 'location': config['ROOT_PATH'], 'custom_domain': config.get("CUSTOM_DOMAIN", None), - 'querystring_expire': 300, + 'querystring_expire': 1200, 'gzip': True, }, ) From db14b32780cdc4bf3a0fb829f3526c5627c59ddf Mon Sep 17 00:00:00 2001 From: moeez96 Date: Wed, 23 Dec 2020 16:22:29 +0500 Subject: [PATCH 10/18] Release v1.59.0 --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index 4adde42025b2..f0d4e0cec050 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -29,7 +29,7 @@ git+https://github.com/edx-solutions/discussion-edx-platform-extensions.git@v2.0 git+https://github.com/edx-solutions/organizations-edx-platform-extensions.git@v2.0.7#egg=organizations-edx-platform-extensions==2.0.7 git+https://github.com/edx-solutions/course-edx-platform-extensions.git@v3.0.0#egg=course-edx-platform-extensions==3.0.0 git+https://github.com/edx-solutions/projects-edx-platform-extensions.git@v3.0.4#egg=projects-edx-platform-extensions==3.0.4 --e git+https://github.com/edx-solutions/api-integration.git@v4.1.6#egg=api-integration==4.1.6 +-e git+https://github.com/edx-solutions/api-integration.git@v4.1.7#egg=api-integration==4.1.7 git+https://github.com/mckinseyacademy/openedx-user-manager-api@v1.2.0#egg=openedx-user-manager-api==1.2.0 openedx-completion-aggregator==2.2.4 From 4e790ae8b6a5a3ca9d2853d32f334adc51a3942e Mon Sep 17 00:00:00 2001 From: moeez96 Date: Thu, 24 Dec 2020 18:28:53 +0500 Subject: [PATCH 11/18] Release v1.59.0 Organizations Ext VB --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index f0d4e0cec050..09c16b0eb5d2 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -26,7 +26,7 @@ git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-g git+https://github.com/edx-solutions/gradebook-edx-platform-extensions.git@2.0.2#egg=gradebook-edx-platform-extensions==2.0.2 git+https://github.com/edx-solutions/mobileapps-edx-platform-extensions.git@v2.0.0#egg=mobileapps-edx-platform-extensions==2.0.0 git+https://github.com/edx-solutions/discussion-edx-platform-extensions.git@v2.0.1#egg=discussion-edx-platform-extensions==2.0.1 -git+https://github.com/edx-solutions/organizations-edx-platform-extensions.git@v2.0.7#egg=organizations-edx-platform-extensions==2.0.7 +git+https://github.com/edx-solutions/organizations-edx-platform-extensions.git@v2.0.8#egg=organizations-edx-platform-extensions==2.0.8 git+https://github.com/edx-solutions/course-edx-platform-extensions.git@v3.0.0#egg=course-edx-platform-extensions==3.0.0 git+https://github.com/edx-solutions/projects-edx-platform-extensions.git@v3.0.4#egg=projects-edx-platform-extensions==3.0.4 -e git+https://github.com/edx-solutions/api-integration.git@v4.1.7#egg=api-integration==4.1.7 From 6908827de8f6f852532d3f1a3dc9096f4b54a898 Mon Sep 17 00:00:00 2001 From: Moeez Zahid Date: Thu, 24 Dec 2020 18:24:56 +0500 Subject: [PATCH 12/18] MCKIN-28814 Include internally whitelisted urls for sso registration inside a frame (#2046) --- common/djangoapps/third_party_auth/decorators.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/third_party_auth/decorators.py b/common/djangoapps/third_party_auth/decorators.py index 622862922fc7..dc5213c83e18 100644 --- a/common/djangoapps/third_party_auth/decorators.py +++ b/common/djangoapps/third_party_auth/decorators.py @@ -9,6 +9,7 @@ from third_party_auth.models import SAMLProviderData from third_party_auth.models import LTIProviderConfig +from edx_solutions_organizations.models import WhitelistedUrls from six.moves.urllib.parse import urlencode, urlparse @@ -35,7 +36,8 @@ def wrapped_view(request, *args, **kwargs): sso_urls = SAMLProviderData.objects.values_list('sso_url', flat=True) sso_urls = [url.rstrip('/') for url in sso_urls] if referer_url in sso_urls: - allowed_urls = ' '.join(settings.THIRD_PARTY_AUTH_FRAME_ALLOWED_FROM_URL) + internal_allowed_urls = [url.url for url in WhitelistedUrls.objects.all()] + allowed_urls = ' '.join(settings.THIRD_PARTY_AUTH_FRAME_ALLOWED_FROM_URL + internal_allowed_urls) x_frame_option = 'ALLOW-FROM {}'.format(allowed_urls) content_security_policy = "frame-ancestors {}".format(allowed_urls) resp['X-Frame-Options'] = x_frame_option From 21f55ef8d7d34ad9e50802440e060b791d074d99 Mon Sep 17 00:00:00 2001 From: Demid Date: Wed, 4 Nov 2020 16:13:53 +0200 Subject: [PATCH 13/18] [BB-2959] Mcka User progress migration endpoint and background task (#1952) * Prototype implementation of user progress merging * Add new outcome, add locks for rows, remove one todo * Rename functions * Rename file with tests * Add mail sending, some refactoring/renaming * Finish tests, add url * Minor improvements * Add tests for view, add logging, fix view * Fix pylint warning * Update docs, add missed assert, fix debug status code Co-authored-by: Paulo Viadanna --- .../user_api/completion/__init__.py | 0 .../djangoapps/user_api/completion/tasks.py | 181 ++++++++++++++ .../user_api/completion/tests/test_tasks.py | 220 ++++++++++++++++++ .../user_api/completion/tests/test_views.py | 50 ++++ .../djangoapps/user_api/completion/views.py | 89 +++++++ openedx/core/djangoapps/user_api/urls.py | 6 + 6 files changed, 546 insertions(+) create mode 100644 openedx/core/djangoapps/user_api/completion/__init__.py create mode 100644 openedx/core/djangoapps/user_api/completion/tasks.py create mode 100644 openedx/core/djangoapps/user_api/completion/tests/test_tasks.py create mode 100644 openedx/core/djangoapps/user_api/completion/tests/test_views.py create mode 100644 openedx/core/djangoapps/user_api/completion/views.py diff --git a/openedx/core/djangoapps/user_api/completion/__init__.py b/openedx/core/djangoapps/user_api/completion/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/core/djangoapps/user_api/completion/tasks.py b/openedx/core/djangoapps/user_api/completion/tasks.py new file mode 100644 index 000000000000..b16766f896fe --- /dev/null +++ b/openedx/core/djangoapps/user_api/completion/tasks.py @@ -0,0 +1,181 @@ +"""Module containing a task for user progress migration""" + +import csv +import logging + +from io import BytesIO + +from celery.task import task +from completion.models import BlockCompletion +from courseware.courses import get_course +from courseware.models import StudentModule +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist +from django.core.mail.message import EmailMessage +from django.db import transaction +from django.utils.translation import ugettext as _ +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from student.models import AnonymousUserId, anonymous_id_for_user, CourseEnrollment +from submissions.models import StudentItem + +log = logging.getLogger(__name__) + +OUTCOME_SOURCE_NOT_FOUND = 'source email not found' +OUTCOME_SOURCE_NOT_ENROLLED = 'source email not enrolled in given course' +OUTCOME_TARGET_NOT_FOUND = 'target email not found' +OUTCOME_TARGET_ALREADY_ENROLLED = 'target email already enrolled in given course' +OUTCOME_COURSE_KEY_INVALID = 'course key invalid' +OUTCOME_COURSE_NOT_FOUND = 'course key not found' +OUTCOME_FAILED_MIGRATION = 'failed to migrate progress' +OUTCOME_MIGRATED = 'migrated' + + +@task(bind=True) +def migrate_progress(self, migrate_list, result_recipients=None): + """ + Task that migrates progress from one user to another + """ + + log.info('Started progress migration. Items to process: %s', len(migrate_list)) + + # Starting migrating completions for each entry + results = [{ + 'course': course, + 'source_email': source, + 'dest_email': target, + 'outcome': _migrate_progress(course, source, target) + } for (course, source, target) in migrate_list] + + results_csv = _create_results_csv(results) + _send_email_with_results(result_recipients, results_csv) + + +def _create_results_csv(results): + """ + Turns results of migration into csv bytestring. + """ + + fieldnames = ['course', 'source_email', 'dest_email', 'outcome'] + + csv_file = BytesIO() + + writer = csv.DictWriter(csv_file, fieldnames) + writer.writeheader() + writer.writerows(results) + + return csv_file.getvalue() + + +def _send_email_with_results(recepients, results_csv): + """ + Triggers email with csv attachment. + """ + + email_subject = _('Progress migration result') + email_text = _('Migration is finished. Please review this email attachment.') + + from_address = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) + recepients = recepients or [settings.SERVER_EMAIL] + attachment_name = 'MigrationResults.csv' + + email = EmailMessage() + email.subject = email_subject + email.body = email_text + email.from_email = from_address + email.to = recepients + email.attach(attachment_name, results_csv, 'text/csv') + + email.send() + + log.info('Email with users progress migration results sent to %s', recepients) + + +def _migrate_progress(course, source, target): + """ + Task that migrates progress from one user to another + """ + log.info('Started progress migration from "%s" to "%s" for "%s" course', source, target, course) + + try: + course_key = CourseKey.from_string(course) + except InvalidKeyError: + log.warning('Migration failed. Invalid course key: %s', course) + return OUTCOME_COURSE_KEY_INVALID + + try: + get_course(course_key) + except ValueError: + log.warning('Migration failed. Course not found:: %s', course_key) + return OUTCOME_COURSE_NOT_FOUND + + try: + source = get_user_model().objects.get(email=source) + except ObjectDoesNotExist: + log.warning('Migration failed. Source user with such email not found: %s', source) + return OUTCOME_SOURCE_NOT_FOUND + + try: + enrollment = CourseEnrollment.objects.select_for_update().get(user=source, course=course_key) + except ObjectDoesNotExist: + log.warning( + 'Migration failed. Source user with email "%s" not enrolled in "%s" course', source.email, course_key + ) + return OUTCOME_SOURCE_NOT_ENROLLED + + try: + target = get_user_model().objects.get(email=target) + except ObjectDoesNotExist: + log.warning('Migration failed. Target user with such email not found: %s', target) + return OUTCOME_TARGET_NOT_FOUND + + if CourseEnrollment.objects.filter(user=target, course=course_key).exists(): + log.warning( + 'Migration failed. Target user with email "%s" already enrolled in "%s" course', target.email, course_key + ) + return OUTCOME_TARGET_ALREADY_ENROLLED + + # Fetch completions for source user + completions = BlockCompletion.user_course_completion_queryset( + user=source, course_key=course_key + ).select_for_update() + + # Fetch edx-submissions data for source user + anonymous_ids = AnonymousUserId.objects.filter(user=source, course_id=course_key).values('anonymous_user_id') + submissions = StudentItem.objects.select_for_update().filter(course_id=course_key, student_id__in=anonymous_ids) + + # Fetch StudentModule table data for source user + student_states = StudentModule.objects.select_for_update().filter(student=source, course_id=course_key) + + # Actually migrate completions and progress + try: + with transaction.atomic(): + # Modify enrollment + enrollment.user = target + enrollment.save() + + # Migrate completions for user + for completion in completions: + completion.user = target + completion.save() + + # Migrate edx-submissions + for submission in submissions: + submission.student_id = anonymous_id_for_user(target, course_key) + submission.save() + + # Migrate StudentModule + for state in student_states: + state.student = target + state.save() + + except Exception: + log.exception("Unexpected error while migrating user progress.") + return OUTCOME_FAILED_MIGRATION + + log.info( + 'User progress in "%s" course successfully migrated from "%s" to "%s"', course_key, source.email, target.email + ) + return OUTCOME_MIGRATED diff --git a/openedx/core/djangoapps/user_api/completion/tests/test_tasks.py b/openedx/core/djangoapps/user_api/completion/tests/test_tasks.py new file mode 100644 index 000000000000..c344dcdc76d6 --- /dev/null +++ b/openedx/core/djangoapps/user_api/completion/tests/test_tasks.py @@ -0,0 +1,220 @@ +"""Unit tests for merging user progress module""" + +import json + +from completion import waffle as completion_waffle +from completion.models import BlockCompletion +from courseware.models import StudentModule +from courseware.tests.factories import StudentModuleFactory + +from django.contrib.auth.models import User +from django.core import mail + + +from openedx.core.djangoapps.user_api.completion.tasks import ( + OUTCOME_COURSE_KEY_INVALID, + OUTCOME_COURSE_NOT_FOUND, + OUTCOME_FAILED_MIGRATION, + OUTCOME_MIGRATED, + OUTCOME_SOURCE_NOT_ENROLLED, + OUTCOME_SOURCE_NOT_FOUND, + OUTCOME_TARGET_ALREADY_ENROLLED, + OUTCOME_TARGET_NOT_FOUND, + migrate_progress, + _create_results_csv, + _migrate_progress, +) +from student.models import CourseEnrollment, anonymous_id_for_user +from submissions import api as sub_api +from submissions.models import StudentItem +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +from mock import patch +import uuid + + +class ProgressMigrationTestCase(ModuleStoreTestCase): + """ + Parent test case for progress migration tests. + """ + + def setUp(self): + super(ProgressMigrationTestCase, self).setUp() + self.course = CourseFactory.create( + org='org', course='course', number='number' + ) + self.course_id = str(self.course.id) + + def _create_user(self, username=None, enrolled=None): + """ + Shortcut to create users and enroll them in some course. + """ + + if not username: + username = uuid.uuid4().hex.upper()[0:6] + user = User.objects.create( + username=username, + email="{}@example.com".format(username) + ) + if enrolled: + CourseEnrollment.enroll(user, self.course.id, mode='audit') + return user + + def _create_user_progress(self, user): + """ + Creates block completion, student module and submission for a given + user. + """ + + block = ItemFactory.create(parent=self.course) + + completion_test_value = 0.4 + + with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True): + BlockCompletion.objects.submit_completion( + user=user, + course_key=block.location.course_key, + block_key=block.location, + completion=completion_test_value, + ) + + StudentModuleFactory.create( + student=user, + course_id=self.course.id, + module_state_key=block.location, + state=json.dumps({}) + ) + + sub_api.create_submission( + { + 'student_id': anonymous_id_for_user(user, self.course.id), + 'course_id': str(self.course.id), + 'item_id': str(block.location), + 'item_type': 'problem', + }, + 'test answer' + ) + + def test_course_invalid_key(self): + source = self._create_user(enrolled=self.course) + target = self._create_user() + self.assertEqual( + _migrate_progress('a+b+c', source.email, target.email), + OUTCOME_COURSE_KEY_INVALID + ) + + def test_course_not_found(self): + source = self._create_user(enrolled=self.course) + target = self._create_user() + self.assertEqual( + _migrate_progress(self.course_id + 'abc', source.email, target.email), + OUTCOME_COURSE_NOT_FOUND + ) + + def test_source_not_found(self): + target = self._create_user() + self.assertEqual( + _migrate_progress(self.course_id, 'dummy@example.com', target.email), + OUTCOME_SOURCE_NOT_FOUND + ) + + def test_source_not_enrolled(self): + source = self._create_user() + target = self._create_user() + self.assertEqual( + _migrate_progress(self.course_id, source.email, target.email), + OUTCOME_SOURCE_NOT_ENROLLED + ) + + def test_target_not_found(self): + source = self._create_user(enrolled=self.course) + self.assertEqual( + _migrate_progress(self.course_id, source.email, 'dummy@example.com'), + OUTCOME_TARGET_NOT_FOUND + ) + + def test_target_already_enrolled(self): + source = self._create_user(enrolled=self.course) + target = self._create_user(enrolled=self.course) + self.assertEqual( + _migrate_progress(self.course_id, source.email, target.email), + OUTCOME_TARGET_ALREADY_ENROLLED + ) + + def test_migrated(self): + source = self._create_user(enrolled=self.course) + target = self._create_user() + + self._create_user_progress(source) + + self.assertEqual( + _migrate_progress(self.course_id, source.email, target.email), + OUTCOME_MIGRATED + ) + + # Check that all user's progress transferred to another user + assert CourseEnrollment.objects.filter(user=target, course=self.course.id).exists() + assert BlockCompletion.user_course_completion_queryset(user=target, course_key=self.course.id).exists() + assert StudentItem.objects.filter( + course_id=self.course.id, student_id=anonymous_id_for_user(target, self.course.id) + ).exists() + assert StudentModule.objects.filter(student=target, course_id=self.course.id).exists() + + def test_failed_migration(self): + source = self._create_user(enrolled=self.course) + target = self._create_user() + with patch.object(CourseEnrollment, 'save') as mock: + mock.side_effect = Exception('Failed to save') + self.assertEqual( + _migrate_progress(self.course_id, source.email, target.email), + OUTCOME_FAILED_MIGRATION + ) + + def test_migrate_progress(self): + """ + Integration test, that checks that: + 1. We send email with correct subject, text and attachment. + 2. Attachment contain correct results set. + """ + + result_csv_rows = [ + { + 'course': self.course_id + 'abc', + 'source_email': self._create_user(enrolled=self.course).email, + 'dest_email': self._create_user().email, + 'outcome': OUTCOME_COURSE_NOT_FOUND + }, + { + 'course': self.course_id, + 'source_email': self._create_user(enrolled=self.course).email, + 'dest_email': self._create_user(enrolled=self.course).email, + 'outcome': OUTCOME_TARGET_ALREADY_ENROLLED + }, + { + 'course': self.course_id, + 'source_email': self._create_user(enrolled=self.course).email, + 'dest_email': self._create_user().email, + 'outcome': OUTCOME_MIGRATED + }, + ] + + migrate_list = [ + (row['course'], row['source_email'], row['dest_email']) + for row + in result_csv_rows + ] + + results_csv = migrate_progress(migrate_list, ['dummy@example.com']) + + self.assertEqual(len(mail.outbox), 1) + + message = mail.outbox[0] + self.assertEqual(message.subject, u'Progress migration result') + self.assertEqual(message.body, u'Migration is finished. Please review this email attachment.') + + attachments = message.attachments + self.assertEqual(len(attachments), 1) + + attachment_content = attachments[0][1] + self.assertEqual(attachment_content, _create_results_csv(result_csv_rows)) diff --git a/openedx/core/djangoapps/user_api/completion/tests/test_views.py b/openedx/core/djangoapps/user_api/completion/tests/test_views.py new file mode 100644 index 000000000000..fabcdc4b3080 --- /dev/null +++ b/openedx/core/djangoapps/user_api/completion/tests/test_views.py @@ -0,0 +1,50 @@ +import mock + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.urlresolvers import reverse +from rest_framework.test import APIClient, APITestCase + +from student.tests.factories import UserFactory + + +class ProgressMigrateAPITestCase(APITestCase): + def setUp(self): + super(ProgressMigrateAPITestCase, self).setUp() + + test_password = 'password' + + self.api_client = APIClient() + self.user = UserFactory(is_staff=True, password=test_password) + self.api_client.login(username=self.user.username, password=test_password) + + self.url = reverse('progress_migrate') + + def test_invalid_csv(self): + invalid_csv = b'course,source_email,wrong_column,outcome\r\n' \ + 'course-v1:a+b+c,source@example.com,target@example.com,\r\n' + + csv_file = SimpleUploadedFile("migrate.csv", invalid_csv, content_type="text/csv") + + response = self.api_client.post( + self.url, {"file": csv_file}, content_type="application/json" + ) + + self.assertEqual(response.status_code, 400) + + @mock.patch('openedx.core.djangoapps.user_api.completion.views.migrate_progress.delay') + def test_migrate_scheduling(self, migrate_progress): + csv = b'course,source_email,dest_email,outcome\r\n' \ + 'course-v1:a+b+c,source@example.com,target@example.com,\r\n' + + csv_file = SimpleUploadedFile("migrate.csv", csv, content_type="text/csv") + + response = self.api_client.post( + self.url, {"recepient_address": self.user.email, "file": csv_file}, format="multipart" + ) + + self.assertEqual(response.status_code, 204) + + migrate_progress.assert_called_with( + [('course-v1:a+b+c', 'source@example.com', 'target@example.com')], + [self.user.email] + ) diff --git a/openedx/core/djangoapps/user_api/completion/views.py b/openedx/core/djangoapps/user_api/completion/views.py new file mode 100644 index 000000000000..e5afe60fe82c --- /dev/null +++ b/openedx/core/djangoapps/user_api/completion/views.py @@ -0,0 +1,89 @@ +import csv +import logging + +from rest_framework.authentication import SessionAuthentication +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser +from rest_framework import permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from .tasks import migrate_progress, OUTCOME_MIGRATED + +log = logging.getLogger(__name__) + + +class MigrateProgressView(APIView): + """ + Migrates user progress for a set of user pairs. + Only admins can use this. + """ + + authentication_classes = ( + JwtAuthentication, + SessionAuthentication, + SessionAuthenticationAllowInactiveUser, + ) + + permission_classes = ( + permissions.IsAuthenticated, + permissions.IsAdminUser, + ) + + def post(self, request): + """ + **Use Cases** + + Migrate progress (enrollment, block completions, submissions, + student module) from one user to another. + + **Example Requests**: + + POST /api/user/v1/completion/migrate/ + { + "recepient_address": "devops@example.com", + } + + ``Content-type`` header of request should be ``multipart/form-data``. + Also note, that "recepient_address" field is not required, + but multipart-encoded csv file is required. + + Example structure of required csv file: + ``` + course,source_email,dest_email,outcome + course-v1:a+b+c,a@example.com,b@example.com,course key invalid + ``` + + **Example POST Responses** + + * If attached csv file doesn't contain any of the required fields + (``course``, ``source_email``, ``dest_email``), status code of the + response will be 400. + + * If migration task successfully scheduled, status code will be 204. + + """ + csv_file = request.FILES['file'] + recepient_address = request.POST.get('recepient_address') + + reader = csv.DictReader(csv_file) + + if not {'course', 'source_email', 'dest_email'}.issubset(set(reader.fieldnames)): + log.warning('Received invalid csv.') + return Response(status=400) + + # Extract list to be used in migration task + migrate_list = [ + (row['course'], row['source_email'], row['dest_email']) for row in reader + if row.get('outcome') != OUTCOME_MIGRATED # Ignore lines marked as migrated + ] + + # Start background task to migrate progress for given users + migrate_progress.delay( + migrate_list, + [recepient_address] if recepient_address else None + ) + + log.info('Scheduled user progress migration. Items to process: %s', len(migrate_list)) + + return Response(status=204) diff --git a/openedx/core/djangoapps/user_api/urls.py b/openedx/core/djangoapps/user_api/urls.py index bdea5668ca59..78b1cf878cb2 100644 --- a/openedx/core/djangoapps/user_api/urls.py +++ b/openedx/core/djangoapps/user_api/urls.py @@ -15,6 +15,7 @@ DeactivateLogoutView, LMSAccountRetirementView ) +from .completion.views import MigrateProgressView from .preferences.views import PreferencesDetailView, PreferencesView from .verification_api.views import IDVerificationStatusView from .validation.views import RegistrationValidationView @@ -150,6 +151,11 @@ RETIREMENT_UPDATE, name='accounts_retirement_update' ), + url( + r'^v1/progress/migrate/$', + MigrateProgressView.as_view(), + name='progress_migrate' + ), url( r'^v1/validation/registration$', RegistrationValidationView.as_view(), From 00f46c29be689a6579041759f5ac2318fbb477c2 Mon Sep 17 00:00:00 2001 From: Demid Date: Wed, 2 Dec 2020 13:00:18 +0200 Subject: [PATCH 14/18] Wrap _migrate_progress in transaction (#2006) --- .../djangoapps/user_api/completion/tasks.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openedx/core/djangoapps/user_api/completion/tasks.py b/openedx/core/djangoapps/user_api/completion/tasks.py index b16766f896fe..0ffdf99c9a55 100644 --- a/openedx/core/djangoapps/user_api/completion/tasks.py +++ b/openedx/core/djangoapps/user_api/completion/tasks.py @@ -93,6 +93,7 @@ def _send_email_with_results(recepients, results_csv): log.info('Email with users progress migration results sent to %s', recepients) +@transaction.atomic def _migrate_progress(course, source, target): """ Task that migrates progress from one user to another @@ -151,25 +152,24 @@ def _migrate_progress(course, source, target): # Actually migrate completions and progress try: - with transaction.atomic(): - # Modify enrollment - enrollment.user = target - enrollment.save() - - # Migrate completions for user - for completion in completions: - completion.user = target - completion.save() - - # Migrate edx-submissions - for submission in submissions: - submission.student_id = anonymous_id_for_user(target, course_key) - submission.save() - - # Migrate StudentModule - for state in student_states: - state.student = target - state.save() + # Modify enrollment + enrollment.user = target + enrollment.save() + + # Migrate completions for user + for completion in completions: + completion.user = target + completion.save() + + # Migrate edx-submissions + for submission in submissions: + submission.student_id = anonymous_id_for_user(target, course_key) + submission.save() + + # Migrate StudentModule + for state in student_states: + state.student = target + state.save() except Exception: log.exception("Unexpected error while migrating user progress.") From b5cb9b7bd738e7862186bdd04321d978f0f2e130 Mon Sep 17 00:00:00 2001 From: Demid Date: Wed, 16 Dec 2020 17:13:34 +0200 Subject: [PATCH 15/18] Update user gradebooks after migration (#2030) --- .../djangoapps/user_api/completion/tasks.py | 6 ++++++ .../user_api/completion/tests/test_tasks.py | 17 +++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/openedx/core/djangoapps/user_api/completion/tasks.py b/openedx/core/djangoapps/user_api/completion/tasks.py index 0ffdf99c9a55..c266a14c9cf4 100644 --- a/openedx/core/djangoapps/user_api/completion/tasks.py +++ b/openedx/core/djangoapps/user_api/completion/tasks.py @@ -15,6 +15,7 @@ from django.core.mail.message import EmailMessage from django.db import transaction from django.utils.translation import ugettext as _ +from gradebook.tasks import update_user_gradebook from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers @@ -175,6 +176,11 @@ def _migrate_progress(course, source, target): log.exception("Unexpected error while migrating user progress.") return OUTCOME_FAILED_MIGRATION + log.info('Updating gradebook for %s user.', source.email) + update_user_gradebook(course, source.id) + log.info('Updating gradebook for %s user.', target.email) + update_user_gradebook(course, target.id) + log.info( 'User progress in "%s" course successfully migrated from "%s" to "%s"', course_key, source.email, target.email ) diff --git a/openedx/core/djangoapps/user_api/completion/tests/test_tasks.py b/openedx/core/djangoapps/user_api/completion/tests/test_tasks.py index c344dcdc76d6..e9013416f1df 100644 --- a/openedx/core/djangoapps/user_api/completion/tests/test_tasks.py +++ b/openedx/core/djangoapps/user_api/completion/tests/test_tasks.py @@ -30,7 +30,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from mock import patch +from mock import call, patch import uuid @@ -148,10 +148,14 @@ def test_migrated(self): self._create_user_progress(source) - self.assertEqual( - _migrate_progress(self.course_id, source.email, target.email), - OUTCOME_MIGRATED - ) + with patch('openedx.core.djangoapps.user_api.completion.tasks.update_user_gradebook') as update_user_gradebook: + outcome = _migrate_progress(self.course_id, source.email, target.email) + + course_key = str(self.course.id) + update_user_gradebook.assert_has_calls([ + call(course_key, source.id), call(course_key, target.id) + ]) + self.assertEqual(outcome, OUTCOME_MIGRATED) # Check that all user's progress transferred to another user assert CourseEnrollment.objects.filter(user=target, course=self.course.id).exists() @@ -205,7 +209,8 @@ def test_migrate_progress(self): in result_csv_rows ] - results_csv = migrate_progress(migrate_list, ['dummy@example.com']) + with patch('openedx.core.djangoapps.user_api.completion.tasks.update_user_gradebook'): + results_csv = migrate_progress(migrate_list, ['dummy@example.com']) self.assertEqual(len(mail.outbox), 1) From c1b2fd41de043dc7aa31f35aced9bb7914890daf Mon Sep 17 00:00:00 2001 From: Muhammad Usman <43761905+musmanmalik@users.noreply.github.com> Date: Fri, 18 Dec 2020 19:31:53 +0500 Subject: [PATCH 16/18] VB GP v2 (#2035) --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index 09c16b0eb5d2..b4aef9779305 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -19,7 +19,7 @@ git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-g -e git+https://github.com/open-craft/xblock-eoc-journal.git@v0.9.4#egg=xblock-eoc-journal==0.9.4 -e git+https://github.com/mckinseyacademy/xblock-scorm.git@v3.2.1#egg=xblock-scorm==3.2.1 -e git+https://github.com/mckinseyacademy/xblock-diagnosticfeedback.git@v0.4.1#egg=xblock-diagnostic-feedback==0.4.1 --e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.10#egg=xblock-group-project-v2==0.10.10 +-e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.11#egg=xblock-group-project-v2==0.10.11 -e git+https://github.com/open-craft/xblock-virtualreality.git@v0.1.5#egg=xblock-virtualreality==0.1.5 -e git+https://github.com/edx/edx-notifications.git@2.0.1#egg=edx-notifications==2.0.1 From cc64ece881a39696d947cd929ba0274e4fcd985f Mon Sep 17 00:00:00 2001 From: Muhammad Usman <43761905+musmanmalik@users.noreply.github.com> Date: Tue, 22 Dec 2020 13:25:16 +0500 Subject: [PATCH 17/18] VB GP v2 (#2039) --- requirements/edx/custom.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/custom.txt b/requirements/edx/custom.txt index b4aef9779305..58b54810158b 100644 --- a/requirements/edx/custom.txt +++ b/requirements/edx/custom.txt @@ -19,7 +19,7 @@ git+https://github.com/edx-solutions/xblock-group-project.git@0.1.3#egg=xblock-g -e git+https://github.com/open-craft/xblock-eoc-journal.git@v0.9.4#egg=xblock-eoc-journal==0.9.4 -e git+https://github.com/mckinseyacademy/xblock-scorm.git@v3.2.1#egg=xblock-scorm==3.2.1 -e git+https://github.com/mckinseyacademy/xblock-diagnosticfeedback.git@v0.4.1#egg=xblock-diagnostic-feedback==0.4.1 --e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.11#egg=xblock-group-project-v2==0.10.11 +-e git+https://github.com/open-craft/xblock-group-project-v2.git@0.10.12#egg=xblock-group-project-v2==0.10.12 -e git+https://github.com/open-craft/xblock-virtualreality.git@v0.1.5#egg=xblock-virtualreality==0.1.5 -e git+https://github.com/edx/edx-notifications.git@2.0.1#egg=edx-notifications==2.0.1 From e7913008f705ae61539ce512cd995043f39b3e61 Mon Sep 17 00:00:00 2001 From: Demid Date: Fri, 25 Dec 2020 13:44:32 +0200 Subject: [PATCH 18/18] Remove stale aggregators (#2049) This commit adds removal of stale aggregators. Without this following queryset: https://github.com/edx-solutions/api-integration/blob/c46d1f3f4834fbcba3913462853552f8acb7815b/edx_solutions_api_integration/courses/utils.py#L128-L133 returns incorrect set of users, which leads to generating of incorrect metrics. --- openedx/core/djangoapps/user_api/completion/tasks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openedx/core/djangoapps/user_api/completion/tasks.py b/openedx/core/djangoapps/user_api/completion/tasks.py index c266a14c9cf4..8597228108f2 100644 --- a/openedx/core/djangoapps/user_api/completion/tasks.py +++ b/openedx/core/djangoapps/user_api/completion/tasks.py @@ -7,6 +7,7 @@ from celery.task import task from completion.models import BlockCompletion +from completion_aggregator.models import Aggregator from courseware.courses import get_course from courseware.models import StudentModule from django.conf import settings @@ -172,6 +173,9 @@ def _migrate_progress(course, source, target): state.student = target state.save() + log.info('Removing stale aggregators for source user.') + Aggregator.objects.filter(user=source, course_key=course_key).delete() + except Exception: log.exception("Unexpected error while migrating user progress.") return OUTCOME_FAILED_MIGRATION