diff --git a/enterprise_catalog/apps/academy/migrations/0004_academy_long_description_en_and_more.py b/enterprise_catalog/apps/academy/migrations/0004_academy_long_description_en_and_more.py new file mode 100644 index 000000000..7616d5a5d --- /dev/null +++ b/enterprise_catalog/apps/academy/migrations/0004_academy_long_description_en_and_more.py @@ -0,0 +1,93 @@ +# Generated by Django 5.2.8 on 2025-12-12 09:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('academy', '0003_mariadb_uuid_conversion'), + ] + + operations = [ + migrations.AddField( + model_name='academy', + name='long_description_en', + field=models.TextField(help_text='Long description of the academy.', null=True), + ), + migrations.AddField( + model_name='academy', + name='long_description_es', + field=models.TextField(help_text='Long description of the academy.', null=True), + ), + migrations.AddField( + model_name='academy', + name='short_description_en', + field=models.TextField(help_text='Short description of the academy.', null=True), + ), + migrations.AddField( + model_name='academy', + name='short_description_es', + field=models.TextField(help_text='Short description of the academy.', null=True), + ), + migrations.AddField( + model_name='academy', + name='title_en', + field=models.CharField(help_text='Academy title', max_length=255, null=True), + ), + migrations.AddField( + model_name='academy', + name='title_es', + field=models.CharField(help_text='Academy title', max_length=255, null=True), + ), + migrations.AddField( + model_name='historicalacademy', + name='long_description_en', + field=models.TextField(help_text='Long description of the academy.', null=True), + ), + migrations.AddField( + model_name='historicalacademy', + name='long_description_es', + field=models.TextField(help_text='Long description of the academy.', null=True), + ), + migrations.AddField( + model_name='historicalacademy', + name='short_description_en', + field=models.TextField(help_text='Short description of the academy.', null=True), + ), + migrations.AddField( + model_name='historicalacademy', + name='short_description_es', + field=models.TextField(help_text='Short description of the academy.', null=True), + ), + migrations.AddField( + model_name='historicalacademy', + name='title_en', + field=models.CharField(help_text='Academy title', max_length=255, null=True), + ), + migrations.AddField( + model_name='historicalacademy', + name='title_es', + field=models.CharField(help_text='Academy title', max_length=255, null=True), + ), + migrations.AddField( + model_name='tag', + name='description_en', + field=models.TextField(help_text='Tag description.', null=True), + ), + migrations.AddField( + model_name='tag', + name='description_es', + field=models.TextField(help_text='Tag description.', null=True), + ), + migrations.AddField( + model_name='tag', + name='title_en', + field=models.CharField(help_text='Tag title', max_length=255, null=True), + ), + migrations.AddField( + model_name='tag', + name='title_es', + field=models.CharField(help_text='Tag title', max_length=255, null=True), + ), + ] diff --git a/enterprise_catalog/apps/academy/migrations/0005_populate_translation_fields.py b/enterprise_catalog/apps/academy/migrations/0005_populate_translation_fields.py new file mode 100644 index 000000000..9c1fcb59f --- /dev/null +++ b/enterprise_catalog/apps/academy/migrations/0005_populate_translation_fields.py @@ -0,0 +1,97 @@ +from django.db import migrations +from django.db.models import Q + + +def batch_update_fields(queryset, field_mapping, batch_size=500): + """ + Helper function to update fields in batches using iterator and bulk_update. + + Args: + queryset: Django queryset (can be filtered) + field_mapping: dict mapping source field -> target field (e.g., {'title': 'title_en'}) + batch_size: number of records to process at once + """ + records_to_update = [] + + for record in queryset.iterator(chunk_size=batch_size): + updated = False + + for source_field, target_field in field_mapping.items(): + source_value = getattr(record, source_field) + target_value = getattr(record, target_field) + + # Only copy if target is empty but source has data + if not target_value and source_value: + setattr(record, target_field, source_value) + updated = True + + if updated: + records_to_update.append(record) + + # Bulk update when batch is full + if len(records_to_update) >= batch_size: + queryset.model.objects.bulk_update( + records_to_update, + list(field_mapping.values()), + batch_size=batch_size + ) + records_to_update = [] + + # Update any remaining records + if records_to_update: + queryset.model.objects.bulk_update( + records_to_update, + list(field_mapping.values()), + batch_size=batch_size + ) + + +def populate_translation_fields(apps, schema_editor): + """ + Populate English translation fields from original fields for Academy and Tag models. + """ + Academy = apps.get_model('academy', 'Academy') + Tag = apps.get_model('academy', 'Tag') + + batch_size = 500 + + # Update Academy records, only fetch those needing translation + academy_filter = ( + Q(title_en__isnull=True) | Q(title_en='') | + Q(short_description_en__isnull=True) | Q(short_description_en='') | + Q(long_description_en__isnull=True) | Q(long_description_en='') + ) + batch_update_fields( + Academy.objects.filter(academy_filter), + { + 'title': 'title_en', + 'short_description': 'short_description_en', + 'long_description': 'long_description_en', + }, + batch_size + ) + + # Update Tag records, only fetch those needing translation + tag_filter = ( + Q(title_en__isnull=True) | Q(title_en='') | + Q(description_en__isnull=True) | Q(description_en='') + ) + batch_update_fields( + Tag.objects.filter(tag_filter), + { + 'title': 'title_en', + 'description': 'description_en', + }, + batch_size + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('academy', '0004_academy_long_description_en_and_more'), + ] + + operations = [ + migrations.RunPython(populate_translation_fields, migrations.RunPython.noop), + ] diff --git a/enterprise_catalog/apps/academy/models.py b/enterprise_catalog/apps/academy/models.py index ac6b26332..a4b1011ba 100644 --- a/enterprise_catalog/apps/academy/models.py +++ b/enterprise_catalog/apps/academy/models.py @@ -6,7 +6,6 @@ from django.db import models from django.utils.translation import gettext as _ from model_utils.models import TimeStampedModel -from simple_history.models import HistoricalRecords from enterprise_catalog.apps.catalog.models import ( ContentMetadata, @@ -52,8 +51,6 @@ class Academy(TimeStampedModel): tags = models.ManyToManyField(Tag, related_name='academies') - history = HistoricalRecords() - class Meta: verbose_name = _('Academy') verbose_name_plural = _('Academies') diff --git a/enterprise_catalog/apps/academy/tests/test_models.py b/enterprise_catalog/apps/academy/tests/test_models.py new file mode 100644 index 000000000..051ee53c6 --- /dev/null +++ b/enterprise_catalog/apps/academy/tests/test_models.py @@ -0,0 +1,37 @@ +""" +Tests for Academy app models. +""" +from django.test import TestCase + +from enterprise_catalog.apps.academy.tests.factories import ( + AcademyFactory, + TagFactory, +) + + +class TagModelTests(TestCase): + """ + Tests for the Tag model. + """ + + def test_tag_str_method(self): + """ + Test that the Tag __str__ method returns the expected format. + """ + tag = TagFactory(title='Test Tag') + expected_str = '' + self.assertEqual(str(tag), expected_str) + + +class AcademyModelTests(TestCase): + """ + Tests for the Academy model. + """ + + def test_academy_creation(self): + """ + Test that an Academy can be created successfully. + """ + academy = AcademyFactory(title='Test Academy') + self.assertIsNotNone(academy.uuid) + self.assertEqual(academy.title, 'Test Academy') diff --git a/enterprise_catalog/apps/academy/translation.py b/enterprise_catalog/apps/academy/translation.py new file mode 100644 index 000000000..cf26cb622 --- /dev/null +++ b/enterprise_catalog/apps/academy/translation.py @@ -0,0 +1,18 @@ +import simple_history +from modeltranslation.translator import TranslationOptions, register + +from enterprise_catalog.apps.academy.models import Academy, Tag + + +@register(Academy) +class AcademyTranslationOptions(TranslationOptions): + fields = ('title', 'short_description', 'long_description',) + + +@register(Tag) +class TagTranslationOptions(TranslationOptions): + fields = ('title', 'description',) + + +# https://django-simple-history.readthedocs.io/en/latest/common_issues.html#usage-with-django-modeltranslation +simple_history.register(Academy, inherit=True) diff --git a/enterprise_catalog/apps/api/v1/serializers.py b/enterprise_catalog/apps/api/v1/serializers.py index 20ac47c3c..fac021cd7 100644 --- a/enterprise_catalog/apps/api/v1/serializers.py +++ b/enterprise_catalog/apps/api/v1/serializers.py @@ -465,6 +465,14 @@ class AcademyTagsListSerializer(serializers.ListSerializer): # pylint: disable= def to_representation(self, obj): # pylint: disable=arguments-renamed """Filter academy tags with no index hits. """ tags = super().to_representation(obj) + + # Map tag IDs to title_en for lookup + title_en_by_id = {tag.id: tag.title_en for tag in obj} + + # Add title_en to each serialized tag + for tag_dict in tags: + tag_dict['title_en'] = title_en_by_id.get(tag_dict['id']) + algolia_client = get_initialized_algolia_client() academy_uuid = self.context.get('academy_uuid') enterprise_uuid = self.context.get('enterprise_uuid') @@ -482,8 +490,12 @@ def to_representation(self, obj): # pylint: disable=arguments-renamed tag_titles_with_results.append(hit.get('value')) tags_with_results = [] for tag in tags: + # Match using both current active language title and English title tag_title = tag['title'] - if tag_title in tag_titles_with_results: + tag_title_en = tag.get('title_en', tag_title) + if tag_title in tag_titles_with_results or tag_title_en in tag_titles_with_results: + # Remove title_en before adding to results (only used for internal matching) + tag.pop('title_en', None) tags_with_results.append(tag) return tags_with_results diff --git a/enterprise_catalog/apps/api/v1/tests/test_views.py b/enterprise_catalog/apps/api/v1/tests/test_views.py index 5ff3d45f3..0f7a8e85c 100644 --- a/enterprise_catalog/apps/api/v1/tests/test_views.py +++ b/enterprise_catalog/apps/api/v1/tests/test_views.py @@ -2133,6 +2133,221 @@ def test_list_with_missing_enterprise_customer(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 0) + # pylint: disable=unused-argument + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @mock.patch('enterprise_catalog.apps.api.v1.serializers.get_initialized_algolia_client') + def test_list_returns_spanish_translations(self, mock_algolia_client, mock_client): + """ + Test that the list endpoint returns Spanish translations when lang=es is provided. + """ + # Set up Spanish translations + self.academy2.title_es = 'Academia de Prueba' + self.academy2.short_description_es = 'Descripción corta en español' + self.academy2.long_description_es = 'Descripción larga en español' + self.academy2.save() + + self.tag1.title_es = 'liderazgo' + self.tag1.description_es = 'Descripción de liderazgo' + self.tag1.save() + + mock_algolia_client.return_value.algolia_index.search_for_facet_values.side_effect = [ + self.mock_algolia_hits, {'facetHits': []} + ] + + params = { + 'enterprise_customer': str(self.enterprise_catalog2.enterprise_customer.uuid), + 'lang': 'es' + } + url = reverse('api:v1:academies-list') + '?{}'.format(urlencode(params)) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 1) + + # Verify Spanish translations are returned + academy_data = response.data['results'][0] + self.assertEqual(academy_data['title'], 'Academia de Prueba') + self.assertEqual(academy_data['short_description'], 'Descripción corta en español') + self.assertEqual(academy_data['long_description'], 'Descripción larga en español') + + # pylint: disable=unused-argument + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @mock.patch('enterprise_catalog.apps.api.v1.serializers.get_initialized_algolia_client') + def test_list_returns_english_translations(self, mock_algolia_client, mock_client): + """ + Test that the list endpoint returns English translations when lang=en is provided. + """ + # Set up English translations + self.academy2.title_en = 'Test Academy' + self.academy2.short_description_en = 'Short description in English' + self.academy2.long_description_en = 'Long description in English' + self.academy2.save() + + mock_algolia_client.return_value.algolia_index.search_for_facet_values.side_effect = [ + self.mock_algolia_hits, {'facetHits': []} + ] + + params = { + 'enterprise_customer': str(self.enterprise_catalog2.enterprise_customer.uuid), + 'lang': 'en' + } + url = reverse('api:v1:academies-list') + '?{}'.format(urlencode(params)) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 1) + + # Verify English translations are returned + academy_data = response.data['results'][0] + self.assertEqual(academy_data['title'], 'Test Academy') + self.assertEqual(academy_data['short_description'], 'Short description in English') + self.assertEqual(academy_data['long_description'], 'Long description in English') + + @mock.patch('enterprise_catalog.apps.api.v1.serializers.get_initialized_algolia_client') + def test_detail_returns_spanish_translations(self, mock_algolia_client): + """ + Test that the detail endpoint returns Spanish translations when lang=es is provided. + """ + # Set up Spanish translations + self.academy2.title_es = 'Academia Detallada' + self.academy2.short_description_es = 'Descripción corta detallada' + self.academy2.long_description_es = 'Descripción larga detallada' + self.academy2.save() + + mock_algolia_client.return_value.algolia_index.search_for_facet_values.side_effect = [ + self.mock_algolia_hits, {'facetHits': []} + ] + + url = reverse('api:v1:academies-detail', kwargs={'uuid': self.academy2.uuid}) + response = self.client.get(url + '?lang=es') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify Spanish translations are returned + self.assertEqual(response.data['title'], 'Academia Detallada') + self.assertEqual(response.data['short_description'], 'Descripción corta detallada') + self.assertEqual(response.data['long_description'], 'Descripción larga detallada') + + @mock.patch('enterprise_catalog.apps.api.v1.serializers.get_initialized_algolia_client') + def test_detail_returns_english_translations(self, mock_algolia_client): + """ + Test that the detail endpoint returns English translations when lang=en is provided. + """ + # Set up English translations + self.academy2.title_en = 'Detailed Academy' + self.academy2.short_description_en = 'Detailed short description' + self.academy2.long_description_en = 'Detailed long description' + self.academy2.save() + + mock_algolia_client.return_value.algolia_index.search_for_facet_values.side_effect = [ + self.mock_algolia_hits, {'facetHits': []} + ] + + url = reverse('api:v1:academies-detail', kwargs={'uuid': self.academy2.uuid}) + response = self.client.get(url + '?lang=en') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify English translations are returned + self.assertEqual(response.data['title'], 'Detailed Academy') + self.assertEqual(response.data['short_description'], 'Detailed short description') + self.assertEqual(response.data['long_description'], 'Detailed long description') + + # pylint: disable=unused-argument + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @mock.patch('enterprise_catalog.apps.api.v1.serializers.get_initialized_algolia_client') + def test_list_ignores_invalid_language_parameter(self, mock_algolia_client, mock_client): + """ + Test that the endpoint ignores invalid language codes and returns default (English) content. + """ + # Set up both English and Spanish translations + self.academy2.title_en = 'English Title' + self.academy2.title_es = 'Título en Español' + self.academy2.short_description_en = 'English description' + self.academy2.short_description_es = 'Descripción en español' + self.academy2.save() + + mock_algolia_client.return_value.algolia_index.search_for_facet_values.side_effect = [ + self.mock_algolia_hits, {'facetHits': []} + ] + + params = { + 'enterprise_customer': str(self.enterprise_catalog2.enterprise_customer.uuid), + 'lang': 'fr' # French is not in MODELTRANSLATION_LANGUAGES + } + url = reverse('api:v1:academies-list') + '?{}'.format(urlencode(params)) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify default English content is returned (invalid language ignored) + academy_data = response.data['results'][0] + self.assertEqual(academy_data['title'], 'English Title') + self.assertEqual(academy_data['short_description'], 'English description') + + @mock.patch('enterprise_catalog.apps.api.v1.serializers.get_initialized_algolia_client') + def test_tags_include_english_title_for_algolia_matching(self, mock_algolia_client): + """ + Test that tags use title_en internally for Algolia matching but don't include it in response. + """ + self.tag1.title_en = 'leadership' + self.tag1.title_es = 'liderazgo' + self.tag1.save() + + mock_algolia_client.return_value.algolia_index.search_for_facet_values.side_effect = [ + self.mock_algolia_hits, {'facetHits': []} + ] + + url = reverse('api:v1:academies-detail', kwargs={'uuid': self.academy2.uuid}) + response = self.client.get(url + '?lang=es') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify tags do NOT include title_en in the response (only used internally for matching) + tags = response.data['tags'] + self.assertGreater(len(tags), 0) + self.assertNotIn('title_en', tags[0]) + + # pylint: disable=unused-argument + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @mock.patch('enterprise_catalog.apps.api.v1.serializers.get_initialized_algolia_client') + def test_list_returns_english_fallback_for_missing_spanish_translations(self, mock_algolia_client, mock_client): + """ + Test that when Spanish is requested but some translations are missing, + the endpoint returns English values for those missing fields (fallback behavior). + """ + # Set up partial Spanish translations - only title has Spanish, descriptions are missing + self.academy2.title_en = 'English Academy Title' + self.academy2.title_es = 'Título de Academia en Español' + self.academy2.short_description_en = 'English short description' + self.academy2.short_description_es = '' # Missing Spanish translation + self.academy2.long_description_en = 'English long description' + self.academy2.long_description_es = None # Missing Spanish translation + self.academy2.save() + + mock_algolia_client.return_value.algolia_index.search_for_facet_values.side_effect = [ + self.mock_algolia_hits, {'facetHits': []} + ] + + params = { + 'enterprise_customer': str(self.enterprise_catalog2.enterprise_customer.uuid), + 'lang': 'es' + } + url = reverse('api:v1:academies-list') + '?{}'.format(urlencode(params)) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 1) + + academy_data = response.data['results'][0] + + # Verify Spanish translation is used where available + self.assertEqual(academy_data['title'], 'Título de Academia en Español') + + # Verify English fallback is used for missing Spanish translations + self.assertEqual(academy_data['short_description'], 'English short description') + self.assertEqual(academy_data['long_description'], 'English long description') + def ddt_cross_product(data_x, data_y): """ diff --git a/enterprise_catalog/apps/api/v1/views/academies.py b/enterprise_catalog/apps/api/v1/views/academies.py index 30c0e724c..2a8db34ce 100644 --- a/enterprise_catalog/apps/api/v1/views/academies.py +++ b/enterprise_catalog/apps/api/v1/views/academies.py @@ -1,3 +1,5 @@ +from django.conf import settings +from django.utils import translation from django.utils.functional import cached_property from edx_rest_framework_extensions.auth.jwt.authentication import ( JwtAuthentication, @@ -19,6 +21,13 @@ class AcademiesReadOnlyViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = AcademySerializer lookup_field = 'uuid' + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + if 'lang' in request.query_params: + lang = request.query_params['lang'] + if lang in settings.MODELTRANSLATION_LANGUAGES: + translation.activate(lang) + @cached_property def request_action(self): return getattr(self, 'action', None) diff --git a/enterprise_catalog/settings/base.py b/enterprise_catalog/settings/base.py index ddff126e6..f0ca8a836 100644 --- a/enterprise_catalog/settings/base.py +++ b/enterprise_catalog/settings/base.py @@ -37,6 +37,7 @@ INSTALLED_APPS = ( 'clearcache', + 'modeltranslation', # For admin integration, modeltranslation must be put before django.contrib.admin 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -173,6 +174,13 @@ root('conf', 'locale'), ) +LANGUAGES = ( + ('en', 'English'), + ('es', 'Spanish') +) +MODELTRANSLATION_LANGUAGES = ('en', 'es') +MODELTRANSLATION_DEFAULT_LANGUAGE = 'en' + # MEDIA CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root diff --git a/requirements/base.in b/requirements/base.in index bc214a3b8..60f1d76b4 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -12,6 +12,7 @@ django-crum django-extensions django-import-export django-model-utils +django-modeltranslation django-simple-history djangorestframework djangorestframework-xml diff --git a/requirements/base.txt b/requirements/base.txt index de77dd402..7ae88eeeb 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -74,7 +74,7 @@ diff-match-patch==20241021 # via django-import-export distro==1.9.0 # via openai -django==5.2.8 +django==5.2.9 # via # -c requirements/common_constraints.txt # -c requirements/constraints.txt @@ -88,6 +88,7 @@ django==5.2.8 # django-import-export # django-log-request-id # django-model-utils + # django-modeltranslation # django-simple-history # django-waffle # djangorestframework @@ -128,7 +129,9 @@ django-model-utils==5.0.0 # -r requirements/base.in # edx-celeryutils # edx-rbac -django-simple-history==3.10.1 +django-modeltranslation==0.19.17 + # via -r requirements/base.in +django-simple-history==3.11.0 # via # -c requirements/constraints.txt # -r requirements/base.in @@ -252,7 +255,7 @@ pyjwt[crypto]==2.10.1 # social-auth-core pymemcache==4.0.0 # via -r requirements/base.in -pymongo==4.15.4 +pymongo==4.15.5 # via edx-opaque-keys pynacl==1.6.1 # via edx-django-utils @@ -293,7 +296,7 @@ rpds-py==0.30.0 # referencing rules==3.5 # via -r requirements/base.in -scikit-learn==1.7.2 +scikit-learn==1.8.0 # via -r requirements/base.in scipy==1.16.3 # via scikit-learn @@ -351,7 +354,7 @@ tzlocal==5.3.1 # via celery uritemplate==4.2.0 # via drf-spectacular -urllib3==2.5.0 +urllib3==2.6.2 # via requests vine==5.1.0 # via diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 7dadc02a8..72cc4cc87 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -18,10 +18,3 @@ Django<6.0 # elastic search changelog: https://www.elastic.co/guide/en/enterprise-search/master/release-notes-7.14.0.html # See https://github.com/openedx/edx-platform/issues/35126 for more info elasticsearch<7.14.0 - -# pip 25.3 is incompatible with pip-tools hence causing failures during the build process -# Make upgrade command and all requirements upgrade jobs are broken due to this. -# See issue https://github.com/openedx/public-engineering/issues/440 for details regarding the ongoing fix. -# The constraint can be removed once a release (pip-tools > 7.5.1) is available with support for pip 25.3 -# Issue to track this dependency and unpin later on: https://github.com/openedx/edx-lint/issues/503 -pip<25.3 diff --git a/requirements/dev.txt b/requirements/dev.txt index 01b5bcf38..d4db499f4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -138,7 +138,7 @@ colorama==0.4.6 # via # -r requirements/test.txt # tox -coverage[toml]==7.12.0 +coverage[toml]==7.13.0 # via # -r requirements/test.txt # pytest-cov @@ -181,7 +181,7 @@ distro==1.9.0 # -r requirements/quality.txt # -r requirements/test.txt # openai -django==5.2.8 +django==5.2.9 # via # -c requirements/common_constraints.txt # -c requirements/constraints.txt @@ -197,6 +197,7 @@ django==5.2.8 # django-import-export # django-log-request-id # django-model-utils + # django-modeltranslation # django-simple-history # django-waffle # djangorestframework @@ -258,7 +259,11 @@ django-model-utils==5.0.0 # -r requirements/test.txt # edx-celeryutils # edx-rbac -django-simple-history==3.10.1 +django-modeltranslation==0.19.17 + # via + # -r requirements/quality.txt + # -r requirements/test.txt +django-simple-history==3.11.0 # via # -c requirements/constraints.txt # -r requirements/quality.txt @@ -505,7 +510,7 @@ path==16.16.0 # via edx-i18n-tools pip-tools==7.5.2 # via -r requirements/pip-tools.txt -platformdirs==4.5.0 +platformdirs==4.5.1 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -597,7 +602,7 @@ pymemcache==4.0.0 # via # -r requirements/quality.txt # -r requirements/test.txt -pymongo==4.15.4 +pymongo==4.15.5 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -616,7 +621,7 @@ pyproject-hooks==1.2.0 # -r requirements/pip-tools.txt # build # pip-tools -pytest==9.0.1 +pytest==9.0.2 # via # -r requirements/test.txt # pytest-cov @@ -696,7 +701,7 @@ rules==3.5 # via # -r requirements/quality.txt # -r requirements/test.txt -scikit-learn==1.7.2 +scikit-learn==1.8.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -820,7 +825,7 @@ uritemplate==4.2.0 # -r requirements/quality.txt # -r requirements/test.txt # drf-spectacular -urllib3==2.5.0 +urllib3==2.6.2 # via # -r requirements/quality.txt # -r requirements/test.txt diff --git a/requirements/django.txt b/requirements/django.txt index bbbbe1416..af7d73551 100644 --- a/requirements/django.txt +++ b/requirements/django.txt @@ -1 +1 @@ -django==5.2.8 +django==5.2.9 diff --git a/requirements/doc.txt b/requirements/doc.txt index 33931734b..2eb1bae18 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -121,7 +121,7 @@ colorama==0.4.6 # via # -r requirements/test.txt # tox -coverage[toml]==7.12.0 +coverage[toml]==7.13.0 # via # -r requirements/test.txt # pytest-cov @@ -153,7 +153,7 @@ distro==1.9.0 # via # -r requirements/test.txt # openai -django==5.2.8 +django==5.2.9 # via # -c requirements/common_constraints.txt # -c requirements/constraints.txt @@ -167,6 +167,7 @@ django==5.2.8 # django-import-export # django-log-request-id # django-model-utils + # django-modeltranslation # django-simple-history # django-waffle # djangorestframework @@ -209,7 +210,9 @@ django-model-utils==5.0.0 # -r requirements/test.txt # edx-celeryutils # edx-rbac -django-simple-history==3.10.1 +django-modeltranslation==0.19.17 + # via -r requirements/test.txt +django-simple-history==3.11.0 # via # -c requirements/constraints.txt # -r requirements/test.txt @@ -394,7 +397,7 @@ packaging==25.0 # pytest # sphinx # tox -platformdirs==4.5.0 +platformdirs==4.5.1 # via # -r requirements/test.txt # pylint @@ -471,7 +474,7 @@ pylint-plugin-utils==0.9.0 # pylint-django pymemcache==4.0.0 # via -r requirements/test.txt -pymongo==4.15.4 +pymongo==4.15.5 # via # -r requirements/test.txt # edx-opaque-keys @@ -483,7 +486,7 @@ pyproject-api==1.10.0 # via # -r requirements/test.txt # tox -pytest==9.0.1 +pytest==9.0.2 # via # -r requirements/test.txt # pytest-cov @@ -551,7 +554,7 @@ rpds-py==0.30.0 # referencing rules==3.5 # via -r requirements/test.txt -scikit-learn==1.7.2 +scikit-learn==1.8.0 # via -r requirements/test.txt scipy==1.16.3 # via @@ -589,7 +592,7 @@ social-auth-core==4.8.1 # social-auth-app-django soupsieve==2.8 # via beautifulsoup4 -sphinx==9.0.0 +sphinx==9.0.4 # via # -r requirements/doc.in # pydata-sphinx-theme @@ -671,7 +674,7 @@ uritemplate==4.2.0 # via # -r requirements/test.txt # drf-spectacular -urllib3==2.5.0 +urllib3==2.6.2 # via # -r requirements/test.txt # requests diff --git a/requirements/pip.txt b/requirements/pip.txt index cd981f413..d3a4a91c9 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -10,7 +10,6 @@ wheel==0.45.1 # The following packages are considered to be unsafe in a requirements file: pip==25.2 # via - # -c requirements/common_constraints.txt # -c requirements/constraints.txt # -r requirements/pip.in setuptools==80.9.0 diff --git a/requirements/production.txt b/requirements/production.txt index 6644088ed..68e682aa0 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -102,7 +102,7 @@ distro==1.9.0 # via # -r requirements/base.txt # openai -django==5.2.8 +django==5.2.9 # via # -r requirements/base.txt # django-celery-results @@ -114,6 +114,7 @@ django==5.2.8 # django-import-export # django-log-request-id # django-model-utils + # django-modeltranslation # django-simple-history # django-waffle # djangorestframework @@ -154,7 +155,9 @@ django-model-utils==5.0.0 # -r requirements/base.txt # edx-celeryutils # edx-rbac -django-simple-history==3.10.1 +django-modeltranslation==0.19.17 + # via -r requirements/base.txt +django-simple-history==3.11.0 # via -r requirements/base.txt django-waffle==5.0.0 # via @@ -219,7 +222,7 @@ exceptiongroup==1.3.1 # celery gevent==25.9.1 # via -r requirements/production.in -greenlet==3.2.4 +greenlet==3.3.0 # via gevent gunicorn==23.0.0 # via -r requirements/production.in @@ -332,7 +335,7 @@ pyjwt[crypto]==2.10.1 # social-auth-core pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.15.4 +pymongo==4.15.5 # via # -r requirements/base.txt # edx-opaque-keys @@ -391,7 +394,7 @@ rpds-py==0.30.0 # referencing rules==3.5 # via -r requirements/base.txt -scikit-learn==1.7.2 +scikit-learn==1.8.0 # via -r requirements/base.txt scipy==1.16.3 # via @@ -477,7 +480,7 @@ uritemplate==4.2.0 # via # -r requirements/base.txt # drf-spectacular -urllib3==2.5.0 +urllib3==2.6.2 # via # -r requirements/base.txt # requests diff --git a/requirements/quality.txt b/requirements/quality.txt index 33cdef6aa..b97152397 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -116,7 +116,7 @@ distro==1.9.0 # via # -r requirements/base.txt # openai -django==5.2.8 +django==5.2.9 # via # -c requirements/common_constraints.txt # -c requirements/constraints.txt @@ -130,6 +130,7 @@ django==5.2.8 # django-import-export # django-log-request-id # django-model-utils + # django-modeltranslation # django-simple-history # django-waffle # djangorestframework @@ -170,7 +171,9 @@ django-model-utils==5.0.0 # -r requirements/base.txt # edx-celeryutils # edx-rbac -django-simple-history==3.10.1 +django-modeltranslation==0.19.17 + # via -r requirements/base.txt +django-simple-history==3.11.0 # via # -c requirements/constraints.txt # -r requirements/base.txt @@ -319,7 +322,7 @@ packaging==25.0 # via # -r requirements/base.txt # kombu -platformdirs==4.5.0 +platformdirs==4.5.1 # via pylint ply==3.11 # via @@ -373,7 +376,7 @@ pylint-plugin-utils==0.9.0 # pylint-django pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.15.4 +pymongo==4.15.5 # via # -r requirements/base.txt # edx-opaque-keys @@ -429,7 +432,7 @@ rpds-py==0.30.0 # referencing rules==3.5 # via -r requirements/base.txt -scikit-learn==1.7.2 +scikit-learn==1.8.0 # via -r requirements/base.txt scipy==1.16.3 # via @@ -520,7 +523,7 @@ uritemplate==4.2.0 # via # -r requirements/base.txt # drf-spectacular -urllib3==2.5.0 +urllib3==2.6.2 # via # -r requirements/base.txt # requests diff --git a/requirements/test.txt b/requirements/test.txt index ef0438f4e..817ea6b23 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -103,7 +103,7 @@ code-annotations==2.3.0 # edx-toggles colorama==0.4.6 # via tox -coverage[toml]==7.12.0 +coverage[toml]==7.13.0 # via # -r requirements/test.in # pytest-cov @@ -144,6 +144,7 @@ distro==1.9.0 # django-import-export # django-log-request-id # django-model-utils + # django-modeltranslation # django-simple-history # django-waffle # djangorestframework @@ -186,7 +187,9 @@ django-model-utils==5.0.0 # -r requirements/base.txt # edx-celeryutils # edx-rbac -django-simple-history==3.10.1 +django-modeltranslation==0.19.17 + # via -r requirements/base.txt +django-simple-history==3.11.0 # via # -c requirements/constraints.txt # -r requirements/base.txt @@ -346,7 +349,7 @@ packaging==25.0 # pyproject-api # pytest # tox -platformdirs==4.5.0 +platformdirs==4.5.1 # via # pylint # tox @@ -406,7 +409,7 @@ pylint-plugin-utils==0.9.0 # pylint-django pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.15.4 +pymongo==4.15.5 # via # -r requirements/base.txt # edx-opaque-keys @@ -416,7 +419,7 @@ pynacl==1.6.1 # edx-django-utils pyproject-api==1.10.0 # via tox -pytest==9.0.1 +pytest==9.0.2 # via # pytest-cov # pytest-django @@ -476,7 +479,7 @@ rpds-py==0.30.0 # referencing rules==3.5 # via -r requirements/base.txt -scikit-learn==1.7.2 +scikit-learn==1.8.0 # via -r requirements/base.txt scipy==1.16.3 # via @@ -568,7 +571,7 @@ uritemplate==4.2.0 # via # -r requirements/base.txt # drf-spectacular -urllib3==2.5.0 +urllib3==2.6.2 # via # -r requirements/base.txt # requests diff --git a/requirements/validation.txt b/requirements/validation.txt index 9861031e1..0f8218a73 100644 --- a/requirements/validation.txt +++ b/requirements/validation.txt @@ -132,7 +132,7 @@ colorama==0.4.6 # via # -r requirements/test.txt # tox -coverage[toml]==7.12.0 +coverage[toml]==7.13.0 # via # -r requirements/test.txt # pytest-cov @@ -169,7 +169,7 @@ distro==1.9.0 # -r requirements/quality.txt # -r requirements/test.txt # openai -django==5.2.8 +django==5.2.9 # via # -c requirements/common_constraints.txt # -c requirements/constraints.txt @@ -184,6 +184,7 @@ django==5.2.8 # django-import-export # django-log-request-id # django-model-utils + # django-modeltranslation # django-simple-history # django-waffle # djangorestframework @@ -242,7 +243,11 @@ django-model-utils==5.0.0 # -r requirements/test.txt # edx-celeryutils # edx-rbac -django-simple-history==3.10.1 +django-modeltranslation==0.19.17 + # via + # -r requirements/quality.txt + # -r requirements/test.txt +django-simple-history==3.11.0 # via # -c requirements/constraints.txt # -r requirements/quality.txt @@ -462,7 +467,7 @@ packaging==25.0 # pyproject-api # pytest # tox -platformdirs==4.5.0 +platformdirs==4.5.1 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -550,7 +555,7 @@ pymemcache==4.0.0 # via # -r requirements/quality.txt # -r requirements/test.txt -pymongo==4.15.4 +pymongo==4.15.5 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -564,7 +569,7 @@ pyproject-api==1.10.0 # via # -r requirements/test.txt # tox -pytest==9.0.1 +pytest==9.0.2 # via # -r requirements/test.txt # pytest-cov @@ -639,7 +644,7 @@ rules==3.5 # via # -r requirements/quality.txt # -r requirements/test.txt -scikit-learn==1.7.2 +scikit-learn==1.8.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -759,7 +764,7 @@ uritemplate==4.2.0 # -r requirements/quality.txt # -r requirements/test.txt # drf-spectacular -urllib3==2.5.0 +urllib3==2.6.2 # via # -r requirements/quality.txt # -r requirements/test.txt