Skip to content

Commit 2922df7

Browse files
authored
feat: refresh_xblock_skills and event handlers for openedx-events (#138)
This commit combines changes implemented in following commits: - cc1889b - ec2a581 The changes were reverted as part of commit a999401 as the upstream MR in openedx-events: openedx/openedx-events#143 was not merged. It is now merged and the dependencies are udpated.
1 parent b4228a7 commit 2922df7

File tree

21 files changed

+984
-27
lines changed

21 files changed

+984
-27
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ Change Log
1313
1414
Unreleased
1515

16+
[1.33.0] - 2023-01-06
17+
---------------------
18+
* https://github.com/openedx/openedx-events/pull/143 merged, so adding back
19+
changes reverted in version 1.32.1
20+
* Added refresh_xblock_skills command to update skills for xblocks.
21+
* Added handlers for openedx-events: XBLOCK_DELETED, XBLOCK_PUBLISHED and XBLOCK_PUBLISHED.
22+
1623
[1.32.3] - 2023-01-05
1724
---------------------
1825
* Added log for EMSI client access token and raising error for error status.

requirements/base.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ django-solo
55
pytz
66
edx-rest-api-client
77
edx-django-utils
8+
edx-opaque-keys
89
celery
910
algoliasearch
1011
django-ses
1112
beautifulsoup4
1213
django-choices
1314
django-filter
15+
openedx-events

requirements/ci.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ charset-normalizer==2.1.1
1010
# via requests
1111
codecov==2.1.12
1212
# via -r requirements/ci.in
13-
coverage==7.0.1
13+
coverage==7.0.2
1414
# via codecov
1515
distlib==0.3.6
1616
# via virtualenv

requirements/dev.txt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ astroid==2.3.3
2323
attrs==22.2.0
2424
# via
2525
# -r requirements/test.txt
26+
# openedx-events
2627
# pytest
2728
beautifulsoup4==4.11.1
2829
# via -r requirements/test.txt
@@ -78,7 +79,7 @@ code-annotations==1.3.0
7879
# via -r requirements/test.txt
7980
codecov==2.1.12
8081
# via -r requirements/ci.txt
81-
coverage[toml]==7.0.1
82+
coverage[toml]==7.0.2
8283
# via
8384
# -r requirements/ci.txt
8485
# -r requirements/test.txt
@@ -105,6 +106,7 @@ django==3.2.16
105106
# djangorestframework
106107
# edx-django-utils
107108
# edx-i18n-tools
109+
# openedx-events
108110
django-choices==1.7.2
109111
# via -r requirements/test.txt
110112
django-crum==0.7.9
@@ -135,6 +137,10 @@ edx-lint==1.5.2
135137
# via
136138
# -c requirements/constraints.txt
137139
# -r requirements/dev.in
140+
edx-opaque-keys[django]==2.3.0
141+
# via
142+
# -r requirements/test.txt
143+
# openedx-events
138144
edx-rest-api-client==5.5.0
139145
# via -r requirements/test.txt
140146
exceptiongroup==1.1.0
@@ -147,6 +153,10 @@ faker==15.3.4
147153
# via
148154
# -r requirements/test.txt
149155
# factory-boy
156+
fastavro==1.7.0
157+
# via
158+
# -r requirements/test.txt
159+
# openedx-events
150160
filelock==3.9.0
151161
# via
152162
# -r requirements/ci.txt
@@ -193,6 +203,8 @@ newrelic==8.5.0
193203
# via
194204
# -r requirements/test.txt
195205
# edx-django-utils
206+
openedx-events==4.1.0
207+
# via -r requirements/test.txt
196208
packaging==22.0
197209
# via
198210
# -r requirements/ci.txt
@@ -242,7 +254,7 @@ pycparser==2.21
242254
# via
243255
# -r requirements/test.txt
244256
# cffi
245-
pydocstyle==6.1.1
257+
pydocstyle==6.2.1
246258
# via -r requirements/dev.in
247259
pygments==2.14.0
248260
# via diff-cover
@@ -264,6 +276,10 @@ pylint-plugin-utils==0.7
264276
# via
265277
# pylint-celery
266278
# pylint-django
279+
pymongo==3.13.0
280+
# via
281+
# -r requirements/test.txt
282+
# edx-opaque-keys
267283
pynacl==1.5.0
268284
# via
269285
# -r requirements/test.txt
@@ -341,6 +357,7 @@ stevedore==4.1.1
341357
# -r requirements/test.txt
342358
# code-annotations
343359
# edx-django-utils
360+
# edx-opaque-keys
344361
testfixtures==7.0.4
345362
# via -r requirements/test.txt
346363
text-unidecode==1.3

requirements/pip-tools.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
2-
# This file is autogenerated by pip-compile with python 3.8
3-
# To update, run:
2+
# This file is autogenerated by pip-compile with Python 3.8
3+
# by the following command:
44
#
55
# make upgrade
66
#

requirements/pip.txt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
#
2-
# This file is autogenerated by pip-compile with python 3.8
3-
# To update, run:
2+
# This file is autogenerated by pip-compile with Python 3.8
3+
# by the following command:
44
#
55
# make upgrade
66
#
7-
wheel==0.38.4
8-
# via -r requirements/pip.in
9-
10-
# The following packages are considered to be unsafe in a requirements file:
117
pip==22.3.1
128
# via -r requirements/pip.in
139
setuptools==65.6.3
1410
# via -r requirements/pip.in
11+
wheel==0.38.4
12+
# via -r requirements/pip.in

requirements/test.txt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ amqp==2.6.1
1313
asgiref==3.6.0
1414
# via django
1515
attrs==22.2.0
16-
# via pytest
16+
# via
17+
# openedx-events
18+
# pytest
1719
beautifulsoup4==4.11.1
1820
# via -r requirements/base.in
1921
billiard==3.6.4.0
@@ -40,7 +42,7 @@ click==8.1.3
4042
# edx-django-utils
4143
code-annotations==1.3.0
4244
# via -r requirements/test.in
43-
coverage[toml]==7.0.1
45+
coverage[toml]==7.0.2
4446
# via pytest-cov
4547
ddt==1.6.0
4648
# via -r requirements/test.in
@@ -55,6 +57,7 @@ ddt==1.6.0
5557
# django-solo
5658
# djangorestframework
5759
# edx-django-utils
60+
# openedx-events
5861
django-choices==1.7.2
5962
# via -r requirements/base.in
6063
django-crum==0.7.9
@@ -75,6 +78,10 @@ edx-django-utils==5.2.0
7578
# via
7679
# -r requirements/base.in
7780
# edx-rest-api-client
81+
edx-opaque-keys[django]==2.3.0
82+
# via
83+
# -r requirements/base.in
84+
# openedx-events
7885
edx-rest-api-client==5.5.0
7986
# via -r requirements/base.in
8087
exceptiongroup==1.1.0
@@ -85,6 +92,8 @@ faker==15.3.4
8592
# via
8693
# -r requirements/test.in
8794
# factory-boy
95+
fastavro==1.7.0
96+
# via openedx-events
8897
idna==3.4
8998
# via requests
9099
iniconfig==1.1.1
@@ -103,6 +112,8 @@ mock==5.0.0
103112
# via -r requirements/test.in
104113
newrelic==8.5.0
105114
# via edx-django-utils
115+
openedx-events==4.1.0
116+
# via -r requirements/base.in
106117
packaging==22.0
107118
# via pytest
108119
pbr==5.11.0
@@ -115,6 +126,8 @@ pycparser==2.21
115126
# via cffi
116127
pyjwt==2.6.0
117128
# via edx-rest-api-client
129+
pymongo==3.13.0
130+
# via edx-opaque-keys
118131
pynacl==1.5.0
119132
# via edx-django-utils
120133
pytest==7.2.0
@@ -164,6 +177,7 @@ stevedore==4.1.1
164177
# via
165178
# code-annotations
166179
# edx-django-utils
180+
# edx-opaque-keys
167181
testfixtures==7.0.4
168182
# via -r requirements/test.in
169183
text-unidecode==1.3

taxonomy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
# 2. MINOR version when you add functionality in a backwards compatible manner, and
1616
# 3. PATCH version when you make backwards compatible bug fixes.
1717
# More details can be found at https://semver.org/
18-
__version__ = '1.32.3'
18+
__version__ = '1.33.0'
1919

2020
default_app_config = 'taxonomy.apps.TaxonomyConfig' # pylint: disable=invalid-name

taxonomy/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ class ProgramMetadataNotFoundError(Exception):
2222
"""
2323

2424

25+
class XBlockMetadataNotFoundError(Exception):
26+
"""
27+
Exception to raise when metadata was not found for an XBlock.
28+
"""
29+
30+
2531
class InvalidCommandOptionsError(Exception):
2632
"""
2733
Exception to raise when incorrect command options are provided.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Management command for refreshing the skills associated with xblocks.
4+
"""
5+
6+
import logging
7+
8+
from django.core.management.base import BaseCommand
9+
from django.utils.translation import gettext as _
10+
from opaque_keys import InvalidKeyError
11+
from opaque_keys.edx.keys import CourseKey, UsageKey
12+
13+
from taxonomy import utils
14+
from taxonomy.choices import ProductTypes
15+
from taxonomy.exceptions import InvalidCommandOptionsError, XBlockMetadataNotFoundError
16+
from taxonomy.models import RefreshXBlockSkillsConfig
17+
from taxonomy.providers.utils import get_course_metadata_provider, get_xblock_metadata_provider
18+
19+
LOGGER = logging.getLogger(__name__)
20+
21+
22+
class Command(BaseCommand):
23+
"""
24+
Command to refresh skills associated with the XBlocks.
25+
26+
Example usage:
27+
$ ./manage.py refresh_xblock_skills --xblock 'xblock-usage-key1' --xblock 'xblock-usage-key2' --commit
28+
$ # To refresh all xblock skills under given courses.
29+
$ ./manage.py refresh_xblock_skills --course 'course-v1:edX+DemoX+1' --course 'course-v1:edX+DemoY+1' --commit
30+
$ # args-from-database means command line arguments will be picked from the database.
31+
$ ./manage.py refresh_xblock_skills --args-from-database
32+
$ # To update all xblocks in all the courses
33+
$ ./manage.py refresh_xblock_skills --all --commit
34+
"""
35+
help = 'Refreshes the skills associated with XBlocks.'
36+
product_type = ProductTypes.XBlock
37+
38+
def add_arguments(self, parser):
39+
"""
40+
Add arguments to the command parser.
41+
"""
42+
parser.add_argument(
43+
'--course',
44+
metavar=_('COURSE_KEY'),
45+
action='append',
46+
help=_('Update skills for XBlocks under given course keys. For eg. course-v1:edX+DemoX.1+2014'),
47+
default=[],
48+
)
49+
parser.add_argument(
50+
'--xblock',
51+
metavar=_('USAGE_KEY'),
52+
action='append',
53+
help=_('Update skills for given Xblock usage keys.'),
54+
default=[],
55+
)
56+
parser.add_argument(
57+
'--args-from-database',
58+
action='store_true',
59+
help=_('Use arguments from the RefreshXBlockSkillsConfig model instead of the command line.'),
60+
)
61+
parser.add_argument(
62+
'--all',
63+
action='store_true',
64+
help=_('Create xblock skill mapping for all xblocks in all the courses.'),
65+
)
66+
parser.add_argument(
67+
'--commit',
68+
action='store_true',
69+
default=False,
70+
help=_('Commits the skills to storage.')
71+
)
72+
73+
def get_args_from_database(self):
74+
"""
75+
Return an options dictionary from the current RefreshXBlockSkillsConfig model.
76+
"""
77+
config = RefreshXBlockSkillsConfig.get_solo()
78+
argv = config.arguments.split()
79+
parser = self.create_parser('manage.py', 'refresh_xblock_skills')
80+
return parser.parse_args(argv).__dict__
81+
82+
@staticmethod
83+
def is_valid_key(key, key_cls, key_cls_str):
84+
"""
85+
Validates usage and course keys.
86+
"""
87+
try:
88+
key_cls.from_string(key)
89+
return True
90+
except InvalidKeyError:
91+
LOGGER.error('[TAXONOMY] Invalid %s: [%s]', key_cls_str, key)
92+
return False
93+
94+
def handle(self, *args, **options):
95+
"""
96+
Entry point for management command execution.
97+
"""
98+
if not (options['args_from_database'] or options['all'] or options['course'] or options['xblock']):
99+
raise InvalidCommandOptionsError(
100+
'Either course, xblock, args_from_database or all argument must be provided.',
101+
)
102+
103+
if options['args_from_database']:
104+
options = self.get_args_from_database()
105+
106+
if options['course'] and options['xblock']:
107+
raise InvalidCommandOptionsError('Either course or xblock argument should be provided and not both.')
108+
109+
LOGGER.info('[TAXONOMY] Refresh XBlock Skills. Options: [%s]', options)
110+
111+
courses = []
112+
xblocks_from_args = []
113+
xblock_provider = get_xblock_metadata_provider()
114+
if options['all']:
115+
courses = get_course_metadata_provider().get_all_courses()
116+
elif options['course']:
117+
courses = [{"key": course} for course in options['course']]
118+
elif options['xblock']:
119+
valid_usage_keys = set(key for key in options['xblock'] if self.is_valid_key(key, UsageKey, "UsageKey"))
120+
xblocks_from_args = xblock_provider.get_xblocks(xblock_ids=list(valid_usage_keys))
121+
if not xblocks_from_args:
122+
raise XBlockMetadataNotFoundError(
123+
'No xblock metadata was found for following xblocks. {}'.format(options['xblock'])
124+
)
125+
else:
126+
raise InvalidCommandOptionsError('Either course, xblock or --all argument must be provided.')
127+
128+
for course in courses:
129+
course_key = course.get("key")
130+
if self.is_valid_key(course_key, CourseKey, "CourseKey"):
131+
xblocks = xblock_provider.get_all_xblocks_in_course(course_key)
132+
LOGGER.info('[TAXONOMY] Refresh xblocks skills process started for course: {course_key}.')
133+
utils.refresh_product_skills(xblocks, options['commit'], self.product_type)
134+
135+
if xblocks_from_args:
136+
LOGGER.info('[TAXONOMY] Refresh XBlock skills process started for xblocks: [%s]', options['xblock'])
137+
utils.refresh_product_skills(xblocks_from_args, options['commit'], self.product_type)

0 commit comments

Comments
 (0)