diff --git a/enterprise_catalog/apps/api/v1/tests/test_views.py b/enterprise_catalog/apps/api/v1/tests/test_views.py index d28817391..1d04ed061 100644 --- a/enterprise_catalog/apps/api/v1/tests/test_views.py +++ b/enterprise_catalog/apps/api/v1/tests/test_views.py @@ -1230,6 +1230,7 @@ def _get_expected_json_metadata(self, content_metadata, is_learner_portal_enable """ content_type = content_metadata.content_type json_metadata = content_metadata.json_metadata.copy() + enrollment_url = '{}/enterprise/{}/{}/{}/enroll/?catalog={}&utm_medium=enterprise&utm_source={}' json_metadata['content_last_modified'] = content_metadata.modified.isoformat()[:-6] + 'Z' if content_metadata.is_exec_ed_2u_course and is_learner_portal_enabled: @@ -1244,8 +1245,6 @@ def _get_expected_json_metadata(self, content_metadata, is_learner_portal_enable enrollment_url = enterprise_proxy_login_url(self.enterprise_slug, next_url=exec_ed_enrollment_url) elif is_learner_portal_enabled and content_type in (COURSE, COURSE_RUN): enrollment_url = '{}/{}/course/{}?{}utm_medium=enterprise&utm_source={}' - else: - enrollment_url = '{}/enterprise/{}/{}/{}/enroll/?catalog={}&utm_medium=enterprise&utm_source={}' marketing_url = '{}?utm_medium=enterprise&utm_source={}' xapi_activity_id = '{}/xapi/activities/{}/{}' diff --git a/enterprise_catalog/apps/api_client/constants.py b/enterprise_catalog/apps/api_client/constants.py index 8f35b7887..0147e89aa 100644 --- a/enterprise_catalog/apps/api_client/constants.py +++ b/enterprise_catalog/apps/api_client/constants.py @@ -26,6 +26,10 @@ ENTERPRISE_CUSTOMER_ENDPOINT = urljoin(ENTERPRISE_API_URL, 'enterprise-customer/') ENTERPRISE_CUSTOMER_CACHE_KEY_TPL = 'customer:{uuid}' STUDIO_API_COURSE_VIDEOS_ENDPOINT = urljoin(settings.STUDIO_BASE_URL, '/api/contentstore/v1/videos/{course_run_key}') +STUDIO_API_VIDEOS_LOCATION_ENDPOINT = urljoin( + settings.STUDIO_BASE_URL, + '/api/contentstore/v1/videos/{course_run_key}/{edx_video_id}/usage' +) # Ecommerce API Client Constants COUPONS_OVERVIEW_ENDPOINT = urljoin( diff --git a/enterprise_catalog/apps/api_client/studio.py b/enterprise_catalog/apps/api_client/studio.py index fb61f7fb8..028b85975 100644 --- a/enterprise_catalog/apps/api_client/studio.py +++ b/enterprise_catalog/apps/api_client/studio.py @@ -1,5 +1,8 @@ from .base_oauth import BaseOAuthClient -from .constants import STUDIO_API_COURSE_VIDEOS_ENDPOINT +from .constants import ( + STUDIO_API_COURSE_VIDEOS_ENDPOINT, + STUDIO_API_VIDEOS_LOCATION_ENDPOINT, +) class StudioApiClient(BaseOAuthClient): @@ -20,3 +23,20 @@ def get_course_videos(self, course_run_key): return self.client.get( STUDIO_API_COURSE_VIDEOS_ENDPOINT.format(course_run_key=course_run_key), ).json() + + def get_video_usage_locations(self, course_run_key, edx_video_id): + """ + Retrieve course video locations for the given course run and edx video id. + + Arguments: + course_run_key (str): The course run key for which to retrieve video metadata. + edx_video_id (str): The edx video id for which to retrieve video metadata. + + Returns: + (list): List of course video locations. + """ + locations = self.client.get( + STUDIO_API_VIDEOS_LOCATION_ENDPOINT.format(course_run_key=course_run_key, edx_video_id=edx_video_id), + ).json().get('usage_locations', []) + + return ([location['url'] for location in locations]) if locations else [] diff --git a/enterprise_catalog/apps/video_catalog/admin.py b/enterprise_catalog/apps/video_catalog/admin.py index 35a077892..a21483839 100644 --- a/enterprise_catalog/apps/video_catalog/admin.py +++ b/enterprise_catalog/apps/video_catalog/admin.py @@ -2,10 +2,13 @@ Admin for video catalog models. """ from django.contrib import admin +from import_export import resources +from import_export.admin import ImportExportModelAdmin from simple_history.admin import SimpleHistoryAdmin from enterprise_catalog.apps.video_catalog.models import ( Video, + VideoShortlist, VideoTranscriptSummary, ) @@ -26,3 +29,21 @@ class VideoAdmin(SimpleHistoryAdmin): """ list_display = ('edx_video_id', 'client_video_id', 'created', 'modified', ) search_fields = ('edx_video_id', 'client_video_id', ) + + +class VideoShortlistResource(resources.ModelResource): + + class Meta: + model = VideoShortlist + import_id_fields = ('video_usage_key',) + fields = ('video_usage_key',) + + +@admin.register(VideoShortlist) +class VideoShortlistAdmin(ImportExportModelAdmin): + """ + Django admin for VideoShortlist. + """ + resource_classes = [VideoShortlistResource] + list_display = ('video_usage_key',) + search_fields = ('video_usage_key',) diff --git a/enterprise_catalog/apps/video_catalog/migrations/0003_videoshortlist_historicalvideo_video_usage_key_and_more.py b/enterprise_catalog/apps/video_catalog/migrations/0003_videoshortlist_historicalvideo_video_usage_key_and_more.py new file mode 100644 index 000000000..c7f1fb99e --- /dev/null +++ b/enterprise_catalog/apps/video_catalog/migrations/0003_videoshortlist_historicalvideo_video_usage_key_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.13 on 2024-06-26 10:19 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('video_catalog', '0002_alter_video_parent_content_metadata'), + ] + + operations = [ + migrations.CreateModel( + name='VideoShortlist', + fields=[ + ('video_usage_key', models.CharField(help_text='Video Xblock Usage Key', max_length=255, primary_key=True, serialize=False)), + ], + options={ + 'verbose_name': 'Shortlisted Video', + 'verbose_name_plural': 'Shortlisted Videos', + }, + ), + migrations.AddField( + model_name='historicalvideo', + name='video_usage_key', + field=models.CharField(default=django.utils.timezone.now, help_text='Video Xblock Usage Key', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='video', + name='video_usage_key', + field=models.CharField(default=django.utils.timezone.now, help_text='Video Xblock Usage Key', max_length=255), + preserve_default=False, + ), + ] diff --git a/enterprise_catalog/apps/video_catalog/models.py b/enterprise_catalog/apps/video_catalog/models.py index 84317b194..7b6e2e863 100644 --- a/enterprise_catalog/apps/video_catalog/models.py +++ b/enterprise_catalog/apps/video_catalog/models.py @@ -65,6 +65,7 @@ class Video(TimeStampedModel): """ edx_video_id = models.CharField(primary_key=True, max_length=255, help_text=_('EdX video id')) client_video_id = models.CharField(max_length=255, help_text=_('Client video id')) + video_usage_key = models.CharField(max_length=255, help_text=_('Video Xblock Usage Key')) parent_content_metadata = models.ForeignKey( ContentMetadata, related_name='videos', @@ -128,3 +129,30 @@ def __str__(self): video_id=str(self.video) ) ) + + +class VideoShortlist(models.Model): + """ + Stores the shortlisted videos for microlearning index and search. + + Example video_usage_key + block-v1:UnivX+QMB1+2T2017+type@video+block@0accf77cf6634c93b0f095f65fed41a1 + + .. no_pii: + """ + video_usage_key = models.CharField(primary_key=True, max_length=255, help_text=_('Video Xblock Usage Key')) + + class Meta: + verbose_name = _("Shortlisted Video") + verbose_name_plural = _("Shortlisted Videos") + app_label = 'video_catalog' + + def __str__(self): + """ + Return human-readable string representation. + """ + return ( + "".format( + usage_key=str(self.video_usage_key) + ) + ) diff --git a/enterprise_catalog/apps/video_catalog/utils.py b/enterprise_catalog/apps/video_catalog/utils.py index 8405450d4..e3330289b 100644 --- a/enterprise_catalog/apps/video_catalog/utils.py +++ b/enterprise_catalog/apps/video_catalog/utils.py @@ -5,23 +5,26 @@ import requests from django.conf import settings +from opaque_keys.edx.keys import UsageKey +from rest_framework.exceptions import ValidationError from enterprise_catalog.apps.ai_curation.openai_client import chat_completions from enterprise_catalog.apps.api_client.studio import StudioApiClient -from enterprise_catalog.apps.catalog.constants import COURSE, COURSE_RUN +from enterprise_catalog.apps.catalog.constants import COURSE_RUN from enterprise_catalog.apps.catalog.models import ContentMetadata -from enterprise_catalog.apps.video_catalog.models import Video +from enterprise_catalog.apps.video_catalog.models import Video, VideoShortlist logger = logging.getLogger(__name__) -def fetch_course_video_metadata(course_run_key): +def fetch_course_video_metadata(course_run_key, video_usage_key): """ Fetch and store video metadata from the Studio service. Arguments: course_run_key (str): The course run key for which to fetch video metadata. + video_usage_key (str): The video usage key for which to fetch video metadata. Raises: (DoesNotExist): If the course run key does not exist in the ContentMetadata model. @@ -29,31 +32,37 @@ def fetch_course_video_metadata(course_run_key): client = StudioApiClient() video_metadata = client.get_course_videos(course_run_key) for video_data in video_metadata.get('previous_uploads', []): - Video.objects.update_or_create( - edx_video_id=video_data['edx_video_id'], - defaults={ - 'client_video_id': video_data['client_video_id'], - 'json_metadata': video_data, - 'parent_content_metadata': ContentMetadata.objects.get( - content_key=course_run_key, content_type=COURSE_RUN + video_usage_locations = client.get_video_usage_locations(course_run_key, video_data['edx_video_id']) + for location in video_usage_locations: + if video_usage_key in location: + Video.objects.update_or_create( + edx_video_id=video_data['edx_video_id'], + video_usage_key=video_usage_key, + defaults={ + 'client_video_id': video_data['client_video_id'], + 'json_metadata': video_data, + 'parent_content_metadata': ContentMetadata.objects.get( + content_key=course_run_key, content_type=COURSE_RUN + ) + } ) - } - ) -def fetch_videos(course_keys): +def fetch_videos(): """ Fetch and store video metadata for multiple course run keys. Arguments: course_keys (list): List of course run keys for which to fetch video metadata. """ - # Import is placed here to avoid circular imports - courses = ContentMetadata.objects.filter(content_key__in=course_keys, content_type=COURSE) - for course in courses: - course_run = course.get_child_records(course).first() - if course_run: - fetch_course_video_metadata(course_run.content_key) + shortlisted_videos = VideoShortlist.objects.all() + for video in shortlisted_videos: + try: + video_usage_key = UsageKey.from_string(video.video_usage_key) + except ValueError: + raise ValidationError('Invalid usage key') # lint-amnesty, pylint: disable=raise-missing-from + course_run_key = str(video_usage_key.context_key) + fetch_course_video_metadata(course_run_key, video.video_usage_key) def get_transcript_summary(transcript: str, max_length: int = 260) -> str: diff --git a/enterprise_catalog/settings/base.py b/enterprise_catalog/settings/base.py index 2ccd28ec6..402cca64a 100644 --- a/enterprise_catalog/settings/base.py +++ b/enterprise_catalog/settings/base.py @@ -56,6 +56,7 @@ 'waffle', 'release_util', 'rules.apps.AutodiscoverRulesConfig', + 'import_export', ) PROJECT_APPS = ( diff --git a/requirements/base.in b/requirements/base.in index b7fd5984a..bc214a3b8 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -10,6 +10,7 @@ django-config-models django-cors-headers django-crum django-extensions +django-import-export django-model-utils django-simple-history djangorestframework diff --git a/requirements/base.txt b/requirements/base.txt index 941b278cd..8c5e716d8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -70,6 +70,8 @@ defusedxml==0.7.1 # djangorestframework-xml # python3-openid # social-auth-core +diff-match-patch==20230430 + # via django-import-export distro==1.9.0 # via openai django==4.2.13 @@ -83,6 +85,7 @@ django==4.2.13 # django-cors-headers # django-crum # django-extensions + # django-import-export # django-log-request-id # django-model-utils # django-simple-history @@ -116,6 +119,8 @@ django-crum==0.7.9 # edx-toggles django-extensions==3.2.3 # via -r requirements/base.in +django-import-export==4.1.0 + # via -r requirements/base.in django-log-request-id==2.1.0 # via -r requirements/base.in django-model-utils==4.5.1 @@ -244,7 +249,7 @@ pyjwt[crypto]==2.8.0 # social-auth-core pymemcache==4.0.0 # via -r requirements/base.in -pymongo==4.7.3 +pymongo==4.8.0 # via edx-opaque-keys pynacl==1.5.0 # via edx-django-utils @@ -325,6 +330,8 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys +tablib==3.5.0 + # via django-import-export text-unidecode==1.3 # via python-slugify threadpoolctl==3.5.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index febd7620b..f728615f8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -162,6 +162,11 @@ diff-cover==4.0.0 # via # -c requirements/constraints.txt # -r requirements/dev.in +diff-match-patch==20230430 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # django-import-export dill==0.3.8 # via # -r requirements/quality.txt @@ -189,6 +194,7 @@ django==4.2.13 # django-crum # django-debug-toolbar # django-extensions + # django-import-export # django-log-request-id # django-model-utils # django-simple-history @@ -238,6 +244,10 @@ django-extensions==3.2.3 # via # -r requirements/quality.txt # -r requirements/test.txt +django-import-export==4.1.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt django-log-request-id==2.1.0 # via # -r requirements/quality.txt @@ -340,7 +350,7 @@ edx-toggles==5.2.0 # -r requirements/test.txt factory-boy==3.3.0 # via -r requirements/test.txt -faker==25.9.1 +faker==26.0.0 # via # -r requirements/test.txt # factory-boy @@ -559,7 +569,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==3.2.3 +pylint==3.2.4 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -587,7 +597,7 @@ pymemcache==4.0.0 # via # -r requirements/quality.txt # -r requirements/test.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -757,6 +767,11 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys +tablib==3.5.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # django-import-export text-unidecode==1.3 # via # -r requirements/quality.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 5c3927a9a..102ab5c55 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -136,6 +136,10 @@ defusedxml==0.7.1 # djangorestframework-xml # python3-openid # social-auth-core +diff-match-patch==20230430 + # via + # -r requirements/test.txt + # django-import-export dill==0.3.8 # via # -r requirements/test.txt @@ -159,6 +163,7 @@ django==4.2.13 # django-cors-headers # django-crum # django-extensions + # django-import-export # django-log-request-id # django-model-utils # django-simple-history @@ -194,6 +199,8 @@ django-dynamic-fixture==4.0.1 # via -r requirements/test.txt django-extensions==3.2.3 # via -r requirements/test.txt +django-import-export==4.1.0 + # via -r requirements/test.txt django-log-request-id==2.1.0 # via -r requirements/test.txt django-model-utils==4.5.1 @@ -272,7 +279,7 @@ edx-toggles==5.2.0 # via -r requirements/test.txt factory-boy==3.3.0 # via -r requirements/test.txt -faker==25.9.1 +faker==26.0.0 # via # -r requirements/test.txt # factory-boy @@ -421,7 +428,7 @@ pydantic-core==2.18.4 # via # -r requirements/test.txt # pydantic -pydata-sphinx-theme==0.15.3 +pydata-sphinx-theme==0.15.4 # via sphinx-book-theme pygments==2.18.0 # via @@ -438,7 +445,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==3.2.3 +pylint==3.2.4 # via # -r requirements/test.txt # edx-lint @@ -460,7 +467,7 @@ pylint-plugin-utils==0.8.2 # pylint-django pymemcache==4.0.0 # via -r requirements/test.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/test.txt # edx-opaque-keys @@ -616,6 +623,10 @@ stevedore==5.2.0 # doc8 # edx-django-utils # edx-opaque-keys +tablib==3.5.0 + # via + # -r requirements/test.txt + # django-import-export text-unidecode==1.3 # via # -r requirements/test.txt diff --git a/requirements/pip.txt b/requirements/pip.txt index 78b740a41..07aabc30d 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -8,7 +8,7 @@ wheel==0.43.0 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==24.1 +pip==24.1.1 # via -r requirements/pip.in -setuptools==70.1.0 +setuptools==70.1.1 # via -r requirements/pip.in diff --git a/requirements/production.txt b/requirements/production.txt index 31a403c60..443e3c5b9 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -95,6 +95,10 @@ defusedxml==0.7.1 # djangorestframework-xml # python3-openid # social-auth-core +diff-match-patch==20230430 + # via + # -r requirements/base.txt + # django-import-export distro==1.9.0 # via # -r requirements/base.txt @@ -108,6 +112,7 @@ django==4.2.13 # django-cors-headers # django-crum # django-extensions + # django-import-export # django-log-request-id # django-model-utils # django-simple-history @@ -141,6 +146,8 @@ django-crum==0.7.9 # edx-toggles django-extensions==3.2.3 # via -r requirements/base.txt +django-import-export==4.1.0 + # via -r requirements/base.txt django-log-request-id==2.1.0 # via -r requirements/base.txt django-model-utils==4.5.1 @@ -324,7 +331,7 @@ pyjwt[crypto]==2.8.0 # social-auth-core pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/base.txt # edx-opaque-keys @@ -433,6 +440,10 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys +tablib==3.5.0 + # via + # -r requirements/base.txt + # django-import-export text-unidecode==1.3 # via # -r requirements/base.txt diff --git a/requirements/quality.txt b/requirements/quality.txt index 33ebf074c..46dc5f730 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -105,6 +105,10 @@ defusedxml==0.7.1 # djangorestframework-xml # python3-openid # social-auth-core +diff-match-patch==20230430 + # via + # -r requirements/base.txt + # django-import-export dill==0.3.8 # via pylint distro==1.9.0 @@ -122,6 +126,7 @@ django==4.2.13 # django-cors-headers # django-crum # django-extensions + # django-import-export # django-log-request-id # django-model-utils # django-simple-history @@ -155,6 +160,8 @@ django-crum==0.7.9 # edx-toggles django-extensions==3.2.3 # via -r requirements/base.txt +django-import-export==4.1.0 + # via -r requirements/base.txt django-log-request-id==2.1.0 # via -r requirements/base.txt django-model-utils==4.5.1 @@ -346,7 +353,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==3.2.3 +pylint==3.2.4 # via # edx-lint # pylint-celery @@ -362,7 +369,7 @@ pylint-plugin-utils==0.8.2 # pylint-django pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/base.txt # edx-opaque-keys @@ -473,6 +480,10 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys +tablib==3.5.0 + # via + # -r requirements/base.txt + # django-import-export text-unidecode==1.3 # via # -r requirements/base.txt diff --git a/requirements/test.txt b/requirements/test.txt index cb2ed2e6d..08a63f0d1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -118,6 +118,10 @@ defusedxml==0.7.1 # djangorestframework-xml # python3-openid # social-auth-core +diff-match-patch==20230430 + # via + # -r requirements/base.txt + # django-import-export dill==0.3.8 # via pylint distlib==0.3.8 @@ -136,6 +140,7 @@ distro==1.9.0 # django-cors-headers # django-crum # django-extensions + # django-import-export # django-log-request-id # django-model-utils # django-simple-history @@ -171,6 +176,8 @@ django-dynamic-fixture==4.0.1 # via -r requirements/test.in django-extensions==3.2.3 # via -r requirements/base.txt +django-import-export==4.1.0 + # via -r requirements/base.txt django-log-request-id==2.1.0 # via -r requirements/base.txt django-model-utils==4.5.1 @@ -240,7 +247,7 @@ edx-toggles==5.2.0 # via -r requirements/base.txt factory-boy==3.3.0 # via -r requirements/test.in -faker==25.9.1 +faker==26.0.0 # via factory-boy filelock==3.15.4 # via @@ -378,7 +385,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==3.2.3 +pylint==3.2.4 # via # edx-lint # pylint-celery @@ -394,7 +401,7 @@ pylint-plugin-utils==0.8.2 # pylint-django pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/base.txt # edx-opaque-keys @@ -518,6 +525,10 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys +tablib==3.5.0 + # via + # -r requirements/base.txt + # django-import-export text-unidecode==1.3 # via # -r requirements/base.txt diff --git a/requirements/validation.txt b/requirements/validation.txt index f9a5e89ed..883e411f6 100644 --- a/requirements/validation.txt +++ b/requirements/validation.txt @@ -150,6 +150,11 @@ defusedxml==0.7.1 # djangorestframework-xml # python3-openid # social-auth-core +diff-match-patch==20230430 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # django-import-export dill==0.3.8 # via # -r requirements/quality.txt @@ -176,6 +181,7 @@ django==4.2.13 # django-cors-headers # django-crum # django-extensions + # django-import-export # django-log-request-id # django-model-utils # django-simple-history @@ -222,6 +228,10 @@ django-extensions==3.2.3 # via # -r requirements/quality.txt # -r requirements/test.txt +django-import-export==4.1.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt django-log-request-id==2.1.0 # via # -r requirements/quality.txt @@ -322,7 +332,7 @@ edx-toggles==5.2.0 # -r requirements/test.txt factory-boy==3.3.0 # via -r requirements/test.txt -faker==25.9.1 +faker==26.0.0 # via # -r requirements/test.txt # factory-boy @@ -508,7 +518,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==3.2.3 +pylint==3.2.4 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -536,7 +546,7 @@ pymemcache==4.0.0 # via # -r requirements/quality.txt # -r requirements/test.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -695,6 +705,11 @@ stevedore==5.2.0 # code-annotations # edx-django-utils # edx-opaque-keys +tablib==3.5.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # django-import-export text-unidecode==1.3 # via # -r requirements/quality.txt